次の方法で共有


CLR 徹底解剖

.NET Framework 4 の基本クラス ライブラリの新機能

Justin Van Patten

Microsoft .NET を使用するほとんどすべてのユーザーが、基本クラス ライブラリ (BCL) を使用しています。より優れた BCL の構築が、ほとんどすべてのマネージ開発者への支援になるということです。このコラムでは、.NET 4 Beta 1 で BCL に追加された新機能に重点を置いて説明します。

新機能のうち 3 つについては、以前の記事で既に取り上げました。まずは、以下に示すこの 3 つの機能について簡単に確認します。

  • コード コントラクトのサポート
  • Parallel Extensions (タスク、同時実行コレクション、および Coordination Data Structures)
  • 組のサポート
    その後、この記事のメインとなる部分では、まだ取り上げていない次の 3 つの新機能について説明します。
  • ファイル IO の機能強化
  • メモリ マップ ファイルのサポート
  • 並べ替えられたセットのコレクション

この記事では、スペースの都合上、BCL の新しい機能強化すべてについて説明することはできませんが、こうした機能強化については、BCL チームのブログ (blogs.msdn.com/bclteam) の今後の記事で取り上げられる予定です。それらを次に示します。

  • 任意の大きさの整数のサポート
  • インターフェイスやデリゲートでのジェネリックの分散の注記
  • 32 ビットおよび 64 ビットのレジストリ ビューへのアクセスおよび揮発性のレジストリ キーの作成のサポート
  • グローバリゼーション データの更新
  • System.Resources のリソース フォールバック ルックアップ ロジックの強化
  • 圧縮の機能強化

Beta 2 には、さらに追加の機能や機能強化がいくつか盛り込まれる予定です。また、そのような機能や機能強化については、Beta 2 の提供が開始されるころに BCL チームのブログで取り上げられる予定です。

コード コントラクト

.NET Framework 4 で BCL に導入された主要な新機能の 1 つは、コード コントラクトです。この新しいライブラリを使用すると、コード内で、言語に依存しない方法で前提条件、事後条件、およびオブジェクトの不変条件を指定することができます。コード コントラクトの詳細については、MSDN Magazine 2009 年 8 月号の Melitta Andersen による「CLR 徹底解剖」コラムを参照してください。また、コード コントラクトの開発者ラボ サイト ( msdn.microsoft.com/devlabs/dd491992.aspx) や、BCL チームのブログ ( blogs.msdn.com/bclteam) もご確認ください。

Parallel Extensions

クライアントにおけるマルチコア プロセッサの重要性は増し、超並列サーバーはより一般的になったので、プログラマがこうしたプロセッサすべてを容易に使用できるようにすることは、かつてないほど重要になっています。.NET 4 で BCL に導入されたもう 1 つの主要な新機能は、並列コンピューティング プラットフォーム チームによって提供されている Parallel Extensions (PFX) 機能です。PFX には Task Parallel Library (TPL)、Coordination Data Structures、同時実行コレクション、および並列 LINQ (PLINQ) が含まれており、これらはすべて、マルチコア コンピューターを活用できるコードの記述をより容易にします。PFX に関する、より詳細な背景知識については、MSDN Magazine 2008 年 10 月号の Stephen Toub と Hazim Shafi による記事「次期バージョンの Visual Studio で強化される並列処理のサポート」( msdn.microsoft.com/magazine/cc817396.aspx) を参照してください。PFX チームのブログ ( blogs.msdn.com/pfxteam) にも、PFX に関する有用な情報が満載です。

.NET 4 で BCL に導入されたもう 1 つの機能は、組のサポートです。組は、実行中に作成できる匿名クラスに似ています。組は、多くの関数型言語や動的言語 (F# や IronPython など) で使用されているデータ構造です。BCL で共通の組型が提供されることにより、言語の相互運用性がさらに促進されます。多くのプログラマが組は便利である (特にメソッドから複数の値を返す場合) と感じているので、C# 開発者や Visual Basic 開発者も組は便利だと感じるでしょう。MSDN Magazine 2009 年 7 月号の Matt Ellis による「CLR 徹底解剖」コラムでは、.NET 4 ににおける、組の新しいサポートについて説明されています。

ファイル IO の機能強化

.NET 4 で導入された新機能のうち、まだ詳しく説明していない機能の 1 つは、System.IO.File に新たに導入された、テキスト ファイルの読み取りと書き込みを行うためのメソッドです。.NET 2.0 以来ずっと、テキスト ファイルの行を読み取る必要がある場合、その方法は、ファイル内のすべての行から成る文字列配列を返す File.ReadAllLines メソッドを呼び出すというものでした。次のコードでは、File.ReadAllLines を使用してテキスト ファイルの行を読み取り、行の長さと行の内容をコンソールに出力しています。

string[] lines = File.ReadAllLines("file.txt");
foreach (var line in lines) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}

残念ながら、このコードにはちょっとした問題があります。問題の原因は、ReadAllLines が配列を返すことです。ReadAllLines は、すべての行を読み取り、返す配列を割り当てなければ、呼び出し元に処理を返すことができません。比較的小さなファイルの場合はこれはそれほど問題になりませんが、何百万もの行がある大きなテキスト ファイルの場合、これは問題になります。電話帳のテキスト ファイルや 900 ページの小説を開くことを想像してみてください。ReadAllLines は、すべての行がメモリに読み込まれるまで他の処理をブロックします。これはメモリの非効率的な使い方であるだけでなく、行の処理を遅延させることにもなります。すべての行がメモリに読み込まれるまでは、最初の行にアクセスできないからです。

この問題を回避するには、TextReader を使用してファイルを開き、ファイルの行を一度に 1 行ずつメモリに読み込みます。これはうまく機能しますが、以下に示すように、File.ReadAllLines を呼び出す方法ほど簡単ではありません。

using (TextReader reader = new StreamReader("file.txt")) {
string line;
while ((line = reader.ReadLine()) != null) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}
}

.NET 4 では、File に ReadLines という (ReadAllLines とは対照的な) 名前の新しいメソッドが追加されています。このメソッドは、string[] ではなく IEnumerable<string> を返します。この新しいメソッドは、一度にすべての行をメモリに読み込むのではなく一度に 1 行ずつ読み取りを行うので、はるかに効率的です。次のコードでは、File.ReadLines を使用して効率的にファイルの行を読み取っています。このコードは、より非効率的な File.ReadAllLines と同じくらいの使いやすさを備えています。

IEnumerable<string> lines = File.ReadLines(@"verylargefile.txt");
foreach (var line in lines) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}

File.ReadLines からはすぐに呼び出し元に処理が返されます。今度は、すべての行がメモリに読み込まれるまで待たなくても、行を反復処理することができます。実際には、foreach ループの各反復でファイルの読み取りが行われています。これにより、(行の読み取り中に行の処理を開始できるため) 認識されるコード パフォーマンスが大幅に向上するだけでなく、行が一度に 1 行ずつ読み取られるため効率もはるかに向上します。File.ReadLines を使用すると、必要に応じて早めにループから抜け出すことができ、読み取る必要のない行を引き続き読み取ることに時間を費やさなくて済むというメリットもあります。

また、File.WriteAllLines に、string[] パラメーターを受け取る既存のオーバーロードと似た、IEnumerable<string> パラメーターを受け取る新しいオーバーロードが追加されました。さらに、IEnumerable<string> を受け取る AppendAllLines という名前の新しいメソッドが追加されました。これは、ファイルの末尾に行を追加するためのメソッドです。こうした新しいメソッドを使用すると、配列を渡すことなく、容易にファイルに書き込みを行ったりファイルの末尾に行を追加したりすることができます。つまり、文字列のコレクションがある場合、それをまず文字列配列に変換する必要はなく、こうしたメソッドに直接渡せるということです。

BCL で、配列の代わりに IEnumerable<T> を使用することによってメリットが得られる部分は他にもあります。ファイル システム列挙 API を例にとってみましょう。以前のバージョンの .NET Framework では、ディレクトリ内のファイルを取得するには、FileInfo オブジェクトの配列を返す DirectoryInfo.GetFiles などのメソッドを呼び出していました。その後、FileInfo オブジェクトを反復処理してファイルに関する情報 (各ファイルの名前と長さなど) を取得することができます。この処理を行うためのコードは、次のようになります。

DirectoryInfo directory = new DirectoryInfo(@"\\share\symbols");
FileInfo[] files = directory.GetFiles();
foreach (var file in files) {
Console.WriteLine("Name={0}, Length={1}", file.Name, file.Length);
}

このコードには問題が 2 つあります。1 つ目の問題は驚くようなことではありません。この問題の原因は、File.ReadAllLines の場合と同様に、GetFiles が配列を返すことです。GetFiles は、ディレクトリ内にあるファイルの一覧全体をファイル システムから取得し、返す配列を割り当てなければ、呼び出し元に処理を返すことができません。つまり、すべてのファイルが取得されるまで待たなければ最初の結果を取得することができないということです。これはメモリの非効率的な使い方です。ディレクトリに 100 万個のファイルが格納されている場合、100 万個のファイルすべてが取得され、100 万個の要素を持つ配列が割り当てられるまで待たなければなりません。

上記のコードに関する 2 つ目の問題はもう少し複雑です。FileInfo のコンストラクターにファイル パスを渡すことにより、FileInfo のインスタンスが作成されます。FileInfo のプロパティ (Length や CreationTime など) は、いずれかのプロパティへの初回アクセス時に初期化されます。プロパティへの初回アクセス時に、FileInfo.Refresh が呼び出され、ファイル システムからファイルのプロパティを取得するためにオペレーティング システムを呼び出します。このような方法を使用することにより、プロパティが使用されない場合はデータを取得するための呼び出しは行われずに済み、使用される場合は、初回アクセス時にデータが古くないことが保証されます。これは、FileInfo のインスタンスが 1 つだけの場合には適切に機能しますが、ディレクトリのコンテンツを列挙する場合は問題となることがあります。ファイルのプロパティを取得するために、ファイル システムへの追加の呼び出しが行われることになるためです。このような追加の呼び出しは、結果をループ処理する際にパフォーマンスの低下をもたらす場合があります。これが特に問題となるのは、リモート ファイル共有のコンテンツを列挙する場合です。ネットワーク経由でリモート コンピューターへの追加のラウンド トリップ呼び出しが行われるためです。

.NET 4 では、この 2 つの問題のどちらに関しても対処が行われています。1 つ目の問題に対処するため、Directory と DirectoryInfo に新しいメソッドが追加されています。これらのメソッドは、配列の代わりに IEnumerable<T> を返します。

File.ReadLines と同様に、IEnumerable<T> ベースのこうした新しいメソッドは、配列ベースの古いメソッドよりもはるかに効率的です。DirectoryInfo.GetFiles メソッドの代わりに .NET 4 の DirectoryInfo.EnumerateFiles メソッドを使用するように更新された次のコードについて考えてみましょう。

DirectoryInfo directory = new DirectoryInfo(@"\\share\symbols");
IEnumerable<FileInfo> files = directory.EnumerateFiles();
foreach (var file in files) {
Console.WriteLine("Name={0}, Length={1}", file.Name, file.Length);
}

GetFiles と違って、EnumerateFiles は、すべてのファイルが取得されるまで他の処理をブロックする必要もなければ、配列を割り当てる必要もありません。EnumerateFiles はすぐに呼び出し元に処理を返し、ユーザーは、ファイル システムから各ファイルが返される際にそのファイルを処理することができます。

2 つ目の問題に対処するため、上記のコードでは、DirectoryInfo は、オペレーティング システムが列挙中に既にファイル システムから取得して提供しているデータを活用するようになっています。DirectoryInfo がファイル システムのコンテンツを取得するために列挙中に呼び出す、基になる Win32 関数には、実際のところ、各ファイルに関する情報 (長さや作成時刻など) が含まれています。上記のコードでは、DirectoryInfo のメソッド (配列ベースの古いメソッドと IEnumerable<T> ベースの新しいメソッドの両方) が返す FileInfo や DirectoryInfo のインスタンスを初期化する際に、このデータを使用しています。つまり、上記のコードでは、file.Length が呼び出される際、ファイルの長さを取得するためにファイル システムへの追加の呼び出しは行われないということです (このデータは既に初期化されているため)。

File と Directory に新しく追加された IEnumerable<T> ベースのメソッドを組み合わせると、いくつかの興味深いシナリオを実現することができます。次のコードについて考えます。

var errorlines =
from file in Directory.EnumerateFiles(@"C:\logs", "*.log")
from line in File.ReadLines(file)
where line.StartsWith("Error:", StringComparison.OrdinalIgnoreCase)
select string.Format("File={0}, Line={1}", file, line);
File.WriteAllLines(@"C:\errorlines.log", errorlines);

このコードでは、Directory と File の新しいメソッドを LINQ と共に使用して、拡張子が .log のファイル、およびそのようなファイルの中でも特に、"Error:" で始まる行を、効率的に検出しています。次に、このクエリでは、結果を、ファイルのパスとエラー行を表示するために書式設定された各文字列から成る新しい一連の文字列にしています。最後に、File.WriteAllLines を使用して、エラー行を errorlines.log という名前の新しいファイルに書き込んでいます。エラー行を配列に変換する必要はありません。このコードのすばらしい点は、非常に効率的である点です。ファイル一覧全体をメモリに読み込んだり、ファイルの内容全体をメモリに読み込んだりはしていません。C:\logs フォルダーに格納されているファイルの数が 10 個でも 100 万個でも、また、ファイルの行数が 10 行でも 100 万行でも、上記のコードは同じように効率的で、使用されるメモリの量は最小限で済みます。

メモリ マップ ファイル

メモリ マップ ファイルのサポートも .NET Framework 4 の新機能の 1 つです。メモリ マップ ファイルは、大きなファイルを編集したり、プロセス間通信 (IPC) 用の共有メモリを作成したりするのに使用できます。メモリ マップ ファイルを使用すると、ファイルをプロセスのアドレス スペースにマップできます。マップが完了したら、アプリケーションでは、ファイルの内容にアクセスしたりファイルの内容を変更したりするためにメモリの読み取りやメモリへの書き込みを簡単に行うことができます。ファイルへのアクセスはオペレーティング システムのメモリ マネージャーを通じて行われるので、ファイルは自動的に複数のページに分割され、これらのページは必要に応じてメモリにページインされたりメモリからページアウトされたりします。これにより、ユーザー自身がメモリ管理を処理する必要はなくなるので、大きなファイルを処理するのがより容易になります。また、これにより、シークを行わずにファイルへの完全なランダム アクセスを行うことが可能になります。

メモリ マップ ファイルは、バッキング ファイルなしで作成することができます。このようなメモリ マップ ファイルは、システムのページング ファイルによってバッキングされます (システムのページング ファイルが存在し、メモリからコンテンツをページアウトする必要がある場合のみ)。メモリ マップ ファイルは、複数のプロセッサ間で共有することができるので、プロセス間通信用の共有メモリを作成するのにうってつけの方法です。各マッピングには対応する名前を付けることができ、他のプロセスでは、この名前を使用して同じメモリ マップ ファイルを開くことができます。

メモリ マップ ファイルを使用するには、まず、以下に示す System.IO.MemoryMappedFiles.MemoryMappedFile クラスの静的ファクトリ メソッドのいずれかを使用して、MemoryMappedFile のインスタンスを作成する必要があります。

  • CreateFromFile
  • CreateNew
  • CreateOrOpen
  • OpenExisting

その後、プロセスのアドレス スペースにファイルを実際にマップする 1 つまたは複数のビューを作成することができます。各ビューではメモリ マップ ファイルの全体または一部をマップすることができ、ビューは部分的に重複することができます。

ファイルのサイズがマッピングに使用できるプロセスの論理メモリ領域 (32 ビット コンピューターでは 2 GB) を超える場合は、複数のビューを使用する必要が生じる場合があります。ビューを作成するには、MemoryMappedFile オブジェクトの CreateViewStream メソッドまたは CreateViewAccessor メソッドを呼び出します。CreateViewStream メソッドは、System.IO.UnmanagedMemoryStream を継承する MemoryMappedFileViewStream のインスタンスを返します。これは、.NET Framework のその他の Stream と同じように使用することができます。一方、CreateViewAccessor は、新しい System.IO.UnmanagedMemoryAccessor クラスを継承する MemoryMappedFileViewAccessor のインスタンスを返します。UnmanagedMemoryAccessor はランダム アクセスを可能にするのに対して、UnmanagedMemoryStream は順次アクセスを可能にします。

新しいメソッド

... System.IO.File 内

  • public static IEnumerable<string>ReadLines(string path)
  • public static void WriteAllLines(string path, IEnumerable<string> contents)
  • public static void AppendAllLines(string path, IEnumerable<string> contents)

... System.IO.Directory 内

  • public static Enumerable<string>EnumerateDirectories(string path)
  • public static IEnumerable<string>EnumerateFiles(string path)
  • public static IEnumerable<string>EnumerateFileSystemEntries(string path)

... System.IO.DirectoryInfo 内

  • public IEnumerable<DirectoryInfo>EnumerateDirectories()
  • public IEnumerable<FileInfo>EnumerateFiles()
  • public IEnumerable<FileSystemInfo>EnumerateFileSystemInfos()

次のサンプルは、メモリ マップ ファイルを使用して IPC 用の共有メモリを作成する方法を示しています。プロセス 1 (図 1 参照) では、メモリ マップ ファイルの名前と容量 (バイト単位) を指定して CreateNew メソッドを呼び出すことにより、MemoryMappedFile の新しいインスタンスを作成しています。これにより、システムのページング ファイルによってバッキングされるメモリ マップ ファイルが作成されます。内部的には、指定された容量は、システムのページ サイズの倍数の中で最も近い値に切り上げられます (興味を持たれた方のためにご説明すると、システムのページ サイズは、.NET 4 で新たに導入された Environment.System- PageSize を使用して取得することができます)。次に、CreateViewStream を使用してビュー ストリームが作成され、BinaryWriter のインスタンスを使用してストリームに "Hello Word!" が書き込まれています。続いて、2 つ目のプロセスが開始されています。

プロセス 2 (図 2 参照) では、適切なメモリ マップ ファイルの名前を指定して OpenExisting メソッドを呼び出すことにより、既存のメモリ マップ ファイルを開いています。その後、ビュー ストリームが作成され、BinaryReader のインスタンスを使用して文字列の読み取りが行われています。

図 1 プロセス 1

using (varmmf = MemoryMappedFile.CreateNew("mymappedfile", 1000))
using (var stream = mmf.CreateViewStream()) {
var writer = new BinaryWriter(stream);
writer.Write("Hello World!");
varstartInfo = new ProcessStartInfo("process2.exe");
startInfo.UseShellExecute = false;
Process.Start(startInfo).WaitForExit();
}

図 2 プロセス 2

using (varmmf = MemoryMappedFile.OpenExisting("mymappedfile"))
using (var stream = mmf.CreateViewStream()) {
var reader = new BinaryReader(stream);
Console.WriteLine(reader.ReadString());
}

SortedSet<T>

.NET Framework 4 では、System.Collections.Concurrent の新しいコレクション (PFX の一部) に加えて、System.Collections.Generic に SortedSet<T> という新しいセット コレクションが含まれています。.NET 3.5 で新たに導入された HashSet<T> と同様に、SortedSet<T> は一意の要素のコレクションです。ただし、HashSet<T> と違って、SortedSet<T> では、要素は並べ替えられた順序で保持されます。

SortedSet<T> は自己均衡のレッドブラック ツリーを使用して実装され、挿入、削除、およびルックアップのパフォーマンスの複雑さは O(log n) となります。一方、HashSet<T> では、挿入、削除、およびルックアップのパフォーマンスは SortedSet<T> より少し優れており、O(1) です。単に汎用的なセットが必要な場合は、ほとんどの場合、HashSet<T> を使用するとよいでしょう。ただし、要素を並べ替えられた順序で保持したり、特定の範囲内にある要素のサブセットを取得したり、最小または最大の要素を取得したりする必要がある場合は、SortedSet<T> を使用します。次のコードは、整数を要素とする SortedSet<T> の使い方を示しています。

var set1 = new SortedSet<int>() { 2, 5, 6, 2, 1, 4, 8 };
bool first = true;
foreach (var i in set1) {
if (first) {
first = false;
}
else {
Console.Write(",");
}
Console.Write(i);
}
// Output: 1,2,4,5,6,8

C# のコレクション初期化構文を使用して、セットの作成と初期化が行われています。整数が任意の順序でセットに追加されていることに注目してください。また、2 が 2 回追加されていることにも注目してください。当然、set1 の要素をループ処理する際には、整数は順序が並べ替えられた状態になっており、セットには 2 が 1 つしか含まれていません。SortedSet<T> の Add メソッドは、HashSet<T> と同様に、bool 型の値を返します。この値を使用して、項目の追加が成功したか (その場合、true が返されます) それとも既にその項目がセットに含まれているため項目は追加されなかったか (その場合、false が返されます) を判断できます。

図 3 は、セット内の最大および最小の要素を取得し、特定の範囲内にある要素のサブセットを取得する方法を示しています。

図 3 最大の要素、最小の要素、および要素のサブセット ビューを取得する

var set1 = new SortedSet<int>() { 2, 5, 6, 2, 1, 4, 8 };
Console.WriteLine("Min: {0}", set1.Min);
Console.WriteLine("Max: {0}", set1.Max);
var subset1 = set1.GetViewBetween(2, 6);
Console.Write("Subset View: ");
bool first = true;
foreach (var i in subset1) {
if (first) {
first = false;
}
else {
Console.Write(",");
}
Console.Write(i);
}
// Output:
// Min: 1
// Max: 8
// Subset View: 2,4,5,6

GetViewBetween メソッドは、元のセットのビューを返します。つまり、ビューに加えられた変更はすべて元のセットに反映されるということです。たとえば、上記のコードで subset1 に 3 を追加した場合、それは実際には set1 に追加されます。指定した範囲の外にある項目をビューに追加することはできません。たとえば、上記のコードで subset1 に 9 を追加しようとすると、ビューの範囲は 2 から 6 までなので Argument-Exception が発生します。

使ってみる

このコラムで説明した BCL の新機能は、.NET Framework 4 で提供される新機能の一部です。こうした機能はプレビューの形で .NET Framework 4 Beta 1 の一部として提供されています。.NET Framework 4 Beta 1 は Visual Studio 2010 Beta 1 と共に msdn.microsoft.com/netframework/dd819232.aspxからダウンロードすることができます。ベータ版をダウンロードし、新機能を使ってみて、 connect.microsoft.com/VisualStudio/content/content.aspx?ContentID=12362でご意見やご感想をお寄せください。また、BCL チームのブログの今後の記事では、今回紹介していない BCL の新機能のいくつかが取り上げられたり、Beta 2 の新機能に関する発表が行われたりする予定なので、BCL チームのブログにもぜひご注目ください。

Justin Van Pattenは、マイクロソフトの CLR チームのプログラム マネージャーとして基本クラス ライブラリを担当しています。Justin には BCL チームのブログ ( blogs.msdn.com/bclteam) から連絡を取ることができます。