クリックして評価とフィードバックをお寄せください
 Advanced Basics: LINQ Enumerable クラ...
Related Articles

クラス階層にオブジェクトを分類するという観点ではなく、テスト駆動開発とモック オブジェクトを使用して、役割と責任の観点からオブジェクト指向コードを設計するにはどうすればよいかについて説明します。

Isaiah Perumalla

MSDN Magazine June 2009

...

Read more!

CLR チームが NET Framework 3.5 SP1 の CLR に対して行った変更と、このサービス パックを使用して既存の CLR 2.0 ベースのアプリケーションを実行した場合のパフォーマンスの向上について説明します。

Surupa Biswas

MSDN Magazine April 2009

...

Read more!

Cobra は Python の子孫であり、特に、動的または静的に型指定されたプログラミング モデルを組み合わせて使用することや、組み込みの単体テスト機能、スクリプト機能、およびいくつかの契約による設計の定義が特徴です。そのすばらしい能力をご紹介します。

Ted Neward

MSDN Magazine June 2009

...

Read more!

Jeremy Miller が引き続き、永続化パターンの解説の一環として、Unit of Work 設計パターンを検討し、永続性の無視にまつわる問題について調べます。

Jeremy Miller

MSDN Magazine June 2009

...

Read more!

ドキュメントやマルチメディア アイテムのような大きい BLOB をデータベースとファイル システムのどちらに格納すればよいかは、常に意見の分かれるところです。SQL Server 2008 ではどちらかを選択する必要はありません。FILESTREAM 記憶域は両方の方法の長所を備えています。

Bob Beauchemin

MSDN Magazine May 2009

...

Read more!

Also by this Author

Microsoft Visual Studio Tools for the Microsoft Office System is a new technology that brings the advanced features of Visual Studio .NET and the .NET Framework to apps built on Microsoft Word 2003 and Excel 2003. Now you can use Visual Basic .NET and C# to write document-centric, managed code solutions that run in-process with Word 2003 or Excel 2003, taking advantage of the rich object models they expose. Along the way you get the benefits of the managed environment in which a fully compiled .NET-based application executes, including code access ...

Read more!

At the beginning of another lovely day of writing courseware in mad pursuit of unrealistic deadlines, I received a frantic call from a business partner. He was at the end of a long consulting project and had several hundred Microsoft® Word documents, all of which required their document properties to be set identically, except the Title property of the document, which was to be based on the document file name, minus the .

Ken Getz

MSDN Magazine March ...

Read more!

This month Ken Getz writes a demo-creation system for Windows-based applications, which he calls a switchboard.

Ken Getz

MSDN Magazine December 2006

...

Read more!

If you've been following Ted Pattison's excellent series of Basic Instincts columns on multithreading and asynchronous behavior, you should by now be an expert on handling the issues involved in working with multiple threads in Windows®-based apps.

Ken Getz

MSDN Magazine March 2005

...

Read more!

今月は、Ken が System.Linq.Enumerable クラスについて詳しく説明し、System.Linq.Enumerable を使用した魔法のようなデータ処理の実行方法を紹介します。

Ken Getz

MSDN Magazine September 2008

...

Read more!

Popular Articles

Kenny Kerr 氏は、Visual C++ を現代的かつ便利に使用できるようになる Visual C++ 2008 Feature Pack を絶賛しています。

Kenny Kerr

MSDN Magazine May 2008

...

Read more!

C# allows developers to embed XML comments into their source files-a useful facility, especially when more than one programmer is working on the same code. The C# parser can expand these XML tags to provide additional information and export them to an external document for further processing. This article shows how to use XML comments and explains the relevant tags. The author demonstrates how to set up your project to export your XML comments into convenient documentation for the benefit of other developers. He also shows how to use comments ...

Read more!

ここでは、新しい F# 言語の基になるいくつかの概念について説明します。F# 言語は、関数型 .NET 言語とオブジェクト指向 .NET 言語の両方の要素を持っています。また、単純なプログラムを記述する方法についても説明します。

Ted Neward

MSDN Magazine Launch 2008

...

Read more!

James Avery does it again with his popular list of developer tools. This time he covers the best Visual Studio add-ins available today that you can download for free.

James Avery

MSDN Magazine December 2005

...

Read more!

This article introduces 10 development tools that can increase your productivity, give you a better understanding of .NET, and maybe even change the way that you develop applications. The tools covered include NUnit to write unit tests, Reflector to examine assemblies, FxCop to police your code, Regulator to build regular expressions, NDoc to create code documentation and five more.

James Avery

MSDN Magazine July 2004

...

Read more!

Advanced Basics
LINQ Enumerable クラス (第 1 部)
Ken Getz

最近のプログラミング プロジェクトの中で、筆者は 1 から 100 までの整数をランダム順に並べたリストを作成する必要がありました。これは間違いなく基本的なコンピュータ サイエンス入門の問題です。最初は 1985 年に授業で作成したのと同じソリューションを使用し、ループや比較を多用して、乱数と複数の配列を生成しました。しかし、最近の宣言型プログラミングに比べて、このソリューションはいかにも煩雑です。
少し調査した後、不具合の部分を削ってわずか 2 行のコードに集約できました。
Dim rnd As New System.Random()
Dim numbers = Enumerable.Range(1, 100). _
  OrderBy(Function() rnd.Next)
この 2 行を実行すると、numbers 変数に 1 から 100 までの整数がランダム順に格納されます。
単純でありながら洗練されたこのソリューションの動作は、System.Linq.Enumerable クラスに依存します。このクラスには拡張メソッドと呼ばれる数十個の共有メソッドがあり、それらのメソッドを使用して、IEnumerable(Of T) インターフェイスを実装したクラスのデータを操作できます。また、この例のように、共有メソッドでデータの新しいコレクションを生成することもできます。
今回と次回のコラムでは、Enumerable クラスの拡張メソッドの例を示します。メンバのオーバーロードの複数バージョンを示し、さまざまなデータ構造体を使用して機能を説明します。これらの拡張メソッドはさまざまな場合に応用でき、非常に便利なので、時間を割いて各メソッドを詳しく検討する価値があります。そうすることで、コレクションを操作する必要があるときにどのようなツールを使用できるかがわかります。
単純な Windows® ベースのアプリケーションをサンプルとして作成しました。このプロジェクトでは、Customers、Categories、および Products のデータを表す単純なクラスと、これらのクラスのインスタンスに SQL Server® Northwind サンプル データベースのデータを設定する SimpleDataContext というクラスを追加しています。また、このクラスは Customers、Categories、および Products というパブリック プロパティを公開し、各プロパティは Customer、Category、および Product インスタンスの汎用リストを保持します (LINQ to SQL デザイナを使用して "本物" の DataContext クラスを作成することもできましたが、それでは Enumerable クラスの拡張メソッドの使用例を示すことができません。つまり、LINQ to SQL のコンテキストで前述のメソッドを呼び出すと、実際には、ここに示したメソッドと同じシグネチャを持ち、LINQ to SQL プロバイダの Queryable クラスの拡張メソッドであるメソッドが呼び出されます。そのような例では、動作が同じでも Enumerable クラスは使用されません)。サンプル アプリケーションをダウンロードし、このアプリケーションを使用しながら記事を読み進めることを強くお勧めします。コードを読むだけでなく直接使用してみることに意義があります。
メソッドの長いリストを扱いやすくするため、メソッドを論理的なグループに分割しました。これから、単一の要素の作成、選択、取得と、シーケンス内でのフィルタ処理、変換、配置、さらにはデータのシーケンスの順序付け、検証、計算、およびセット操作の実行を処理するメソッドを詳しく見ていきます。以下の各セクションで、Enumerable クラスの 1 つまたは複数のメソッドを使用するさまざまな例を実際に試してください。最後の例まで試すことで、Enumerable クラスのすべてのメソッドを使用し、複雑なメソッドのオーバーロードを示す多くの例を詳しく検証できます。
ここに示す例では、通常、Enumerable クラスの拡張メソッドを呼び出したときの戻り値型をコードで指定するのではなく、Visual Basic® でデータ型を推定できるようになっています。また、サンプル コードでは SQL Express 上にインストールした Northwind サンプル データベースを使用しています。サンプルを実行する場合は、これと同じ環境を用意するか、サンプル アプリケーションを変更する必要があります。

Enumerable の内部
Enumerable クラスは、作成するすべての LINQ クエリで重要な役割を果たします。たとえば、プロジェクトにサンプル プロジェクトと同様の Product クラスと Product インスタンスの汎用リストが含まれているとします。サンプルから SimpleDataContext クラスのインスタンスを作成し、Products リストにデータを入力して、次のようなクエリを記述します。
Dim db As New SimpleDataContext()
Dim query = _
  From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Select prod.ProductName
これをよく見たとき、次のことに気付いた読者もいるでしょう。つまり、SQL に似たこの構文を特定の型のインスタンスでの一連のメソッド呼び出しに変換する処理を Visual Basic コンパイラで内部的に行う必要があります。実際、この例では、SimpleDataContext クラスの Products プロパティは List(Of Product) 型です。また、既におわかりかもしれませんが、List(Of T) 型は IEnumerable(Of T) インターフェイスを実装します。LINQ のキーワードはコンパイラによって対応する Enumerable 型の拡張メソッドの呼び出しに変換されます。具体的には、前の LINQ クエリを次のように書き直すことによって、まったく同一の一連の命令を作成できます。
Dim query = _
  db.Products _
  .Where(Function(prod) CBool(prod.CategoryID = 1)) _
  .OrderBy(Function(prod) prod.ProductName) _
  .Select(Function(prod) prod.ProductName)
コンパイラによって作成された中間言語 (IL) を見ると、Visual Basic のキーワードを使用した LINQ クエリで、Visual Basic のキーワードが Enumerable クラスの対応するメソッドの呼び出しに変換されていることがわかります。この変換の結果として System.Collections.Generic.List(Of T) クラスの動作が拡張されます。実際のところ、その判別は簡単です。この特定のクラスのメンバに関するドキュメントで拡張メソッドの一覧を参照すると、ほとんどの拡張メソッドが Enumerable クラスで定義されていることがわかります。
これは何を意味するのでしょうか。Enumerable クラスの拡張メソッドは、他の多くのクラス (Array や List など) を処理できます。Enumerable クラスのメソッドは、LINQ クエリを作成するだけでなく、配列などのデータ構造体の動作を操作するためにも使用できます。
Enumerable クラスの拡張メソッドのドキュメントを読むと、各メンバが共有メンバであることが記載されています。結果的に、メソッドの呼び出しには 2 つの選択肢があります。1 つは宣言型のメソッドとして呼び出す方法であり、次のように、拡張されたクラスのインスタンスを渡します (results 変数が、Count メソッドで処理できる型であると想定)。
Dim itemCount = _
  Enumerable.Count(results, _
  Function(prod) prod.CategoryID = 3)
もう 1 つは、Count をコレクション自体のインスタンス メソッドと同様に扱って呼び出す方法です。この方法で拡張メソッドを呼び出す場合、第 1 パラメータを指定する必要はありません。
Dim itemCount = _
  results.Count(Function(prod) _
  prod.CategoryID = 3)
Visual Basic と C# は、キーワードから拡張メソッド呼び出しへの変換方法が異なります。C# よりも Visual Basic の方が、LINQ クエリ内で使用できるキーワードが多数あります。たとえば、Visual Basic では、次のように Take キーワードを使用して LINQ クエリを作成できます。
Dim db As New SimpleDataContext()
Dim query = _
  From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Take 10 _
  Select prod.ProductName
または、次のように Enumerable クラスの Take 拡張メソッドを使用することもできます。
Dim query = _
  (From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Select prod.ProductName).Take(10)
C# には Take キーワードがないため、Take メソッドを明示的に呼び出す以外に方法はありません。図 1 は、Visual Basic の LINQ クエリ キーワードと対応する Enumerable クラスのメソッドのマッピングを示しています。
Visual Basic の LINQ クエリ キーワード Enumerable のメソッド
Aggregate … Into … All All
Aggregate … Into … Any Any
Aggregate … Into … Average Average
Aggregate … Into … Count Count
Aggregate … Into … LongCount LongCount
Aggregate … Into … Max Max
Aggregate … Into … Min Min
Aggregate … Into … Sum Sum
Distinct Distinct
Group By GroupBy
Group Join GroupJoin
Order By OrderBy
Order By … Descending OrderByDescending
Order By (複数フィールドの場合) ThenBy
Order By … Descending (複数フィールドの場合) ThenByDescending
Select Select
Skip Skip
Take Take
Take While TakeWhile
Where Where

シーケンスを作成する
Enumerable クラスには、新しいシーケンスを作成するために存在する、拡張メソッドではないいくつかの共有メソッドがあります。これらの単純なメソッドを検証することから始めます。
Enumerable.Range メソッドは、整数の新しい順序付きリストを作成します。リストの開始値と項目数を指定すると、IEnumerable(Of Integer) 型のシーケンスが返されます。このメソッドを使用して、開始値と終了値の間の数字をランダム順に並べたリストを取得するという、この記事の基本問題を解決できます。
次のコードでは、Range メソッドを使用してリストを設定し、単純なラムダ式で Enumerable.OrderBy メソッドを呼び出して、シーケンスをランダム順に並べます。
' From RangeDemo in the sample:
Dim rnd As New System.Random
Dim items = Enumerable.Range(1, 10)
Dim randomList = _
  items.OrderBy(Function() rnd.Next())
サンプル プロシージャを複数回実行したとき、返される結果は毎回異なりますが、コードを実行するたびに次のようなリストが得られます。
6, 8, 7, 1, 2, 9, 10, 4, 3, 5
Enumerable.Reverse メソッドは、入力シーケンスを逆順に返します。次のコードは、Reverse メソッドを Enumerable クラスの共有メソッドとして呼び出した場合と、Enumerable.Range メソッドの呼び出しから返されたリストのインスタンス メソッドとして呼び出した場合を示しています。
'From ReverseDemo in the sample:
Dim items = Enumerable.Range(1, 10)
Dim reversed = items.Reverse()
reversed = Enumerable.Reverse(items)
どちらの方法で Reverse メソッドを呼び出しても、返されるリストは次のとおりです。
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
Enumerable.Repeat メソッドは、指定の値を指定の回数だけ繰り返したリストを作成します。
'From RepeatDemo in the sample:
Dim repeated = Enumerable.Repeat("Hello", 5)
前述のコードを呼び出すと、repeated 変数に次の項目が格納されます。
Hello, Hello, Hello, Hello, Hello
Enumerable.Empty メソッドは、空の IEnumerable(Of T) を作成し、指定したデータ型を格納するように設定するときに使用します。このメソッドを使用してコレクションの空のインスタンスを作成し、必要に応じてそのインスタンスに項目を追加できます。コレクションを空のコレクションとして初期化すると、参照が Nothing であるかどうかを考慮する必要がありません。
'From EmptyDemo in the sample:
Dim emptyList = _
 Enumerable.Empty(Of Customer)()

シーケンスを選択する
Enumerable クラスには、一連の要素を選択し、その要素を入力シーケンスから出力シーケンスに射影するための 2 種類の拡張メソッドがあります。Enumerable.Select メソッドは、各要素を新しい形式に単純に射影します。次の例では、Northwind Customers テーブルから米国の顧客のシーケンスを取得し、その結果を顧客名の番号付きリストのシーケンスに射影します。
' From SelectDemo in the sample:
Dim db As New SimpleDataContext
Dim results = _
  From cust In db.Customers _
  Where cust.Country = "USA"
Dim selectResults = results.Select( _
  Function(cust, index) _
  String.Format("{0}. {1}", _
  index + 1, cust.ContactName))
Select メソッドに複数のオーバーロード バージョンがあることに注意してください。このコードを実行すると、selectResults に次のような値のリストが格納されます。
1. Howard Snyder
2. Yoshi Latimer
3. John Steel
4. Jaime Yorres
5. Fran Wilson
6. Rene Phillips
7. Paula Wilson
8. Jose Pavarotti
9. Art Braunschweiger
10. Liz Nixon
11. Liu Wong
12. Helvetius Nagy
13. Karl Jablonski
Enumerable.SelectMany メソッドは、項目のコレクションを受け取り (その各項目が項目のコレクションである場合がある)、2 次元の入力を 1 次元の出力シーケンスに変換します。このメソッドは、たとえば、カテゴリのリスト (各カテゴリに製品が含まれている) を受け取り、対応するカテゴリ情報を含む製品リストを返す場合に便利です。次の例では、含まれる製品数が 7 個未満であるカテゴリのリストを取得し、その結果を射影して、各製品に関する情報とカテゴリ情報を含む 1 つのリストを返します。
' From SelectManyDemo in the sample:
Dim db As New SimpleDataContext
Dim categories = _
  From cat In db.Categories Where cat.Products.Count < 7
Dim manyResults As IEnumerable(Of String) = _
  categories.SelectMany(Function(cat, index) _
  cat.Products.Select( _
  Function(prod As Product) String.Format("{0}. Category {1}: {2}", _
    index, prod.CategoryID, prod.ProductName)))
この場合、SelectMany メソッドに渡したラムダ式が特定のカテゴリへの参照を受け取り、各カテゴリを順番に処理します。SelectMany メソッドのこの特定のオーバーロードにより、カテゴリのインデックスとカテゴリをラムダ式に渡します。カテゴリとインデックスを受け取ったラムダ式がカテゴリの Products プロパティの Select メソッドを呼び出し、カテゴリに含まれる各製品がメソッドで処理されます。このサンプルでは次の文字列リストを含むシーケンスが返されます。SelectMany の呼び出しによって、元のカテゴリと製品の階層が単一の値リストにフラット化されます。
0. Category 6: Mishi Kobe Niku
0. Category 6: Alice Mutton
0. Category 6: Thüringer Rostbratwurst
0. Category 6: Perth Pasties
0. Category 6: Tourtière
0. Category 6: Pâté chinois
1. Category 7: Uncle Bob's Organic Dried Pears
1. Category 7: Tofu
1. Category 7: Rössle Sauerkraut
1. Category 7: Manjimup Dried Apples
1. Category 7: Longlife Tofu

単一の要素を取得する
シーケンスから単一の要素を取得して処理することが必要な場合が多くあります。Enumerable クラスには、シーケンスの内容をフィルタ処理して単一の要素を取り出すメソッドがいくつかあります。Enumerable.First メソッドはシーケンスの最初の要素を返し、Enumerable.Last メソッドはシーケンスの最後の要素を返します。Enumerable.Single メソッドは、単一の要素を指定した関数またはラムダ式を受け取り、1 つの要素を返します。Enumerable.ElementAt メソッドは、シーケンス内の特定のインデックス位置にある単一要素を返します。
要求した要素がシーケンスに存在しない場合、または Single メソッドでの制限の結果として要素が 0 個しか存在しないか複数個存在する場合、これらの各メソッドは例外をスローします。また、これらの各メソッドでは、ラムダ式または関数を指定して入力データを制限することもできます。いずれの場合も、返されるシーケンスに含まれる要素は 1 つでなければなりません。
Enumerable クラスの同様のメソッドに、可能な場合は単一要素を返し、単一要素を返すことができない場合はシーケンスの型の既定メンバを返すメソッドもあります。これに該当するメソッドは、Enumerable.FirstOrDefault、Enumerable.LastOrDefault、Enumerable.SingleOrDefault、および Enumerable.ElementAtOrDefault です。図 2 は、これらのメソッドの動作を示しています。サンプルを実行すると、次のデータが出力ストリームに書き込まれます。
ALFKI: Maria Anders
ALFKI: Maria Anders
Last in USA: Karl Jablonski
Sequence contains no elements
Specified argument was out of the range of valid values.
Parameter name: index
CustomerID = XXXXX is Nothing: True
Customer 2 in USA is Nothing: False
Customer 200 in USA is Nothing: True
' From SingleElementDemo in the sample:

Dim oneCustomer As Customer
Dim sw As New StringWriter

Dim db As New SimpleDataContext

Dim query0 = From cust In db.Customers

Dim query1 = _
  From cust In db.Customers _
  Where cust.CustomerID = "ALFKI"

Dim query2 = _
  From cust In db.Customers _
  Where cust.Country = "USA"

Dim query3 = _
  From cust In db.Customers _
  Where cust.CustomerID = "XXXXX"

' This works fine, finds the customer whose CustomerID is ALFKI:
oneCustomer = query1.Single
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)

' You can supply an expression to filter, when calling 
' the First, Last, Single, and so on, methods:
oneCustomer = query0.Single( _
  Function(cust) cust.CustomerID = "ALFKI")
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)

' This works fine, finds the last customer in the USA:
oneCustomer = query2.Last
sw.WriteLine("Last in USA: {0}", oneCustomer.ContactName)

' These two raise exceptions:
Try
  ' There's no customer use customer ID is "XXXXX":
  oneCustomer = query3.Single
Catch ex As Exception
  sw.WriteLine(ex.Message)
End Try

Try
  ' There aren't 200 customers in the USA:
  oneCustomer = query2.ElementAt(200)
Catch ex As Exception
  sw.WriteLine(ex.Message)
End Try

' These don't raise exceptions, but return default values
' if they fail:
oneCustomer = query3.SingleOrDefault
sw.WriteLine("CustomerID = XXXXX is Nothing: {0}", _
             oneCustomer Is Nothing)

' Because there are at least two customers in the USA,
' method returns a valid, single customer:
oneCustomer = query2.ElementAtOrDefault(2)
sw.WriteLine("Customer 2 in USA is Nothing: {0}", _
             oneCustomer Is Nothing)

oneCustomer = query2.ElementAtOrDefault(200)
sw.WriteLine("Customer 200 in USA is Nothing: {0}", _
             oneCustomer Is Nothing)

シーケンスをフィルタ処理する
Enumerable.Where メソッド、Enumerable.Distinct メソッド、または Enumerable.OfType メソッドを使用して既存のシーケンスの内容をフィルタ処理し、出力シーケンスで元のデータのサブセットを返すことができます。OfType メソッドは、指定された型に基づいて入力シーケンスをフィルタ処理します。フォーム上の特定の型のコントロールのみを操作する場合を考えてみましょう。この場合、OfType メソッドを使用して、フォームで公開されるコントロールのコレクションを制限し、該当するコントロールのサブセットのみを反復処理することができます。
次の例では、汎用リストにデータを設定し、整数の出力リストを取得して、文字列のリストを取得します。
' From OfTypeDemo in the sample:
Dim items As New List(Of Object)
items.Add("January")
items.Add(0)
items.Add("Monday")
items.Add(3)
items.Add(5)
items.Add("September")

Dim numbers = items.OfType(Of Integer)()
Dim strings = items.OfType(Of String)()
このコードを実行すると、2 つの出力リストに次のデータが格納されます。
0, 3, 5
January, Monday, September
Enumerable.Where メソッドでは、入力シーケンスをフィルタ処理する条件を指定できます。2 つ目のオーバーロード バージョンとして、コレクションの各項目のインデックスにアクセスし、インデックスに基づいてフィルタ処理を行うことができます。次の例では、Where メソッドと Select メソッドの両方を使用します。最初にフィルタ処理を行い、次に結果を射影して、C:\Windows フォルダ内の 100 バイトより小さいファイルのシーケンスを取得します。
' From WhereDemo in the sample:
Dim files As IEnumerable(Of FileInfo) = _
  New DirectoryInfo("C:\Windows").GetFiles()
Dim fileResults = _
  files _
  .Where(Function(file) file.Length < 100) _
  .Select(Function(file) _
    String.Format("{0} ({1})", file.Name, file.Length))
配列は IEnumerable インターフェイスを実装するため、Where メソッドを使用して他のコレクションと同様に配列の内容もフィルタ処理できます。この場合、コードで GetFiles メソッドによって返される FileInfo オブジェクトの配列をフィルタ処理します。また、例では、Where メソッドの呼び出しの後に Select メソッドの呼び出しをカスケードしています。筆者のコンピュータでこのサンプルを実行したところ、次の結果が返されました。
Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)
S86D5A060.tmp (48)
setuperr.log (0)
Where メソッドの 2 つ目のオーバーロードを使用して、処理対象の各入力項目のインデックスにアクセスできます。たとえば、次の Where メソッドを呼び出すと、サイズが 100 バイト未満で、最初の 20 個の入力ファイルのいずれかであるファイルが返されます。
' From WhereDemo in the sample:
fileResults = _
  files _
  .Where(Function(file, index) _
    (file.Length < 100) And (index < 20)) _
  .Select(Function(file) _
    String.Format("{0} ({1})", file.Name, file.Length))
この場合、筆者のコンピュータでの実行結果は次のようになりました (不明なファイルは、処理された最初の 20 個のファイルから除外されています)。
Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)
Enumerable.Distinct メソッドを使用すると、フィルタ処理を実行し、入力リストの一意の項目のみを出力シーケンスに格納できます。文字列や数値などの単純な値の場合、この処理は簡単です。
では、複雑なオブジェクト リストの場合、ランタイム エンジンは、オブジェクトが重複しているかどうかを判断するためにどのように比較を行うのでしょうか。この場合、単純にインスタンス変数を比較しても、オブジェクトのメモリ アドレスが比較されるだけなので意味がありません。複雑なオブジェクトの場合は比較処理を指定する必要があります。つまり、比較を実行するための IEqualityComparer(Of T) を実装するクラスのインスタンスを指定します。この問題は、Enumerable クラスの複数のメソッドに当てはまるため、このコラムで後ほどもう一度説明します。
たとえば、顧客情報を含むシーケンスが存在し、顧客の所在国を示す一意のリストが必要だとします。単純な所在国リストがあれば、既定の比較処理を使用して文字列を比較できます。ところが、ここにあるのは顧客リストです。Select メソッドを使用してリストを射影し、所在国のリストだけを取得することも可能ですが、その場合、リストを使用してその他の計算を行うための残りのすべてのデータが失われます。
サンプル プロジェクトには、IEqualityComparer(Of Customer) インターフェイスを実装する図 3 のような CustomerCountryComparer クラスがあります。次のサンプル コードは Distinct メソッドを使用する異なる 2 つの方法を示しています。
Public Class CustomerCountryComparer
  Implements IEqualityComparer(Of Customer)

  Public Function Equals1( _
    ByVal x As Customer, ByVal y As Customer) As Boolean _
    Implements IEqualityComparer(Of Customer).Equals

    Return x.Country.Equals(y.Country)

  End Function

  Public Function GetHashCode1( _
    ByVal obj As Customer) As Integer _
    Implements IEqualityComparer(Of Customer).GetHashCode

    Return obj.Country.GetHashCode

  End Function
End Class
最初の方法では、顧客リストをまず国名のリストに射影し、次に既定の文字列比較処理を使用して、国名リストを一意なデータのリストに集約しています。2 つ目の方法では、明示的に CustomerCountryComparer クラスを使用して比較を実行し、一意なリストを作成して、重複のない顧客リストを文字列のリストに射影しています。
' From DistinctDemo in the sample:
Dim db As New SimpleDataContext

Dim countries As IEnumerable(Of String) = _
  db.Customers _
  .Select(Function(cust) cust.Country) _
  .Distinct()

countries = customers _
  .Distinct(New CustomerCountryComparer) _
  .Select(Function(cust) cust.Country)
必要に応じて空のシーケンスを適切に処理します。Enumerable.DefaultIfEmpty メソッドは入力シーケンス全体を返すか、シーケンスが空の場合は既定値のインスタンスを返します。このメソッドを使用すると、シーケンスの型を持つ項目が常に少なくとも 1 つ存在するようになります。
図 4 のプロシージャは、DefaultIfEmpty メソッドの 1 つの使用例を示しています。このコードでは、noCustomers 変数に空のリストを設定し (国名が "XXX" の顧客は存在しません)、DefaultIfEmpty メソッドを呼び出す noCustomers1 リストを作成します。このサンプル コードを実行すると、次の出力が生成されます。
noCustomers contains 0 element(s). noCustomers1 contains 1 element(s).
XXXXX
' From DefaultIfEmptyDemo in the sample:
Dim db As New SimpleDataContext
Dim sw As New StringWriter

Dim noCustomers = _
  From cust In db.Customers Where cust.Country = "XXX"
Dim noCustomers1 = _
  noCustomers.DefaultIfEmpty()

Dim results = _
  String.Format( _
  "noCustomers contains {0} element(s). " & _
  "noCustomers1 contains {1} element(s).", _
  noCustomers.Count, noCustomers1.Count)
sw.WriteLine(results)

' You can specify the exact value to use if the 
' collection is empty:
noCustomers1 = noCustomers.DefaultIfEmpty( _
  New Customer With {.CustomerID = "XXXXX"})
For Each cust In noCustomers1
  sw.WriteLine(cust.CustomerID)
Next
noCustomers1 には 1 つの要素 (情報が設定されていない 1 件の Customer) が格納されています。2 番目のメソッド呼び出しのように、シングルトン項目の既定値を指定して DefaultIfEmpty メソッドを呼び出すことができます。

シーケンスを順序付けする
Enumerable.OrderBy、Enumerable.ThenBy、Enumerable.OrderByDescending、および Enumerable.ThenByDescending を使用して最初の順序付けを指定し、次に、2 番目以降の 1 つまたは複数の順序付け (昇順または降順) を指定できます。このサンプルでは、顧客のシーケンスから開始して、これらのメソッドのいくつかを使用して順序付けを適用します。
' From OrderByDemo in the sample:
Dim db As New SimpleDataContext
Dim customers = _
  From cust In db.Customers _
  Where cust.ContactTitle = "Owner"

' Sort first by country, then by city descending, then by name:
Dim results = _
  customers _
  .OrderBy(Function(cust) cust.Country) _
  .ThenByDescending(Function(cust As Customer) cust.City) _
  .ThenBy(Function(cust As Customer) cust.ContactName) _
  .Select(Function(cust) String.Format("({0}, {1}) {2}", _
  cust.Country, cust.City, cust.ContactName))
Visual Basic では、ThenBy メソッドおよび ThenByDescending メソッドの呼び出し時にラムダ式のパラメータのデータ型を指定しなくてもエラーになりませんが、データ型を指定しない場合、IntelliSense® の機能を利用できません。最初の順序付けメソッドのみがラムダ式のパラメータのデータ型を推定できます。おかしなことですが、Select メソッドでは、長大に連鎖したメソッドのリストの最後であってもデータ型が正しく推定されます。このサンプルでは、肩書きが "Owner" の顧客を最初に国順、次に都市の降順、最後に各都市の連絡先名順に並べ替えます。
(Denmark, Kobenhavn) Jytte Petersen
(France, Paris) Marie Bertrand
(France, Nantes) Janine Labrune
(France, Marseille) Laurence Lebihan
(Germany, Köln) Henriette Pfalzheim
(Mexico, México D.F.) Ana Trujillo
(Mexico, México D.F.) Antonio Moreno
(Mexico, México D.F.) Miguel Angel Paolino
(Norway, Stavern) Jonas Bergulfsen
(Poland, Warszawa) Zbyszek Piestrzeniewicz
...

シーケンスを検証する
アプリケーションによっては、シーケンス内で特定の条件が満たされているかどうかを検証する必要があります。条件を満たしている要素が存在するか。すべての要素が条件を満たしているか。特定の項目がシーケンス内に存在するか。2 つのシーケンスは等しいか。Enumerable クラスには、このすべての情報を取得するメソッドがあります。
シーケンスに要素が 1 つでも存在するかどうかを確認するには、パラメータを指定しないで Enumerable.Any メソッドを呼び出します。条件を指定し、その条件に該当する要素がシーケンス内に存在するかどうかを確認するには、このメソッドに関数を渡します。入力シーケンスのいずれかの要素が存在するか、指定された条件に一致する要素が存在する場合、True が返されます (図 5 を参照)。
' From AnyDemo in sample:
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

' Determine if a list has any elements:
Dim anyElements = results.Any()

' Call as extension method or shared method
' of IEnumerable:
Dim matchingElements = _
  results.Any(Function(prod) prod.ProductName.StartsWith("M"))
matchingElements = _
  Enumerable.Any(results, _
  Function(prod As Product) prod.ProductName.StartsWith("M"))
シーケンスのすべてのメンバが条件を満たしているかどうかを確認するには、条件を指定する関数を渡して Enumerable.All メソッドを呼び出します。次の例では、結果コレクション内のすべての要素の UnitsInStock の値が 5 より大きいかどうかを確認しています。
' From AllDemo in sample:
' Is it true that all products have more than 5 units in stock?
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1
Dim allElements = _
  results.All(Function(prod) CBool(prod.UnitsInStock > 5))
シーケンスに特定の要素が含まれているかどうかを確認するには、Enumerable.Contains を呼び出します。検索対象が文字列や整数などの単純な値である場合、既定の比較処理を使用し、検索する値を指定するだけで済みます。より複雑なオブジェクトがシーケンス内に存在するかどうかを確認する場合は、IEqualityComparer(Of T) を実装するクラスのインスタンスを指定する必要があります。
図 6 のサンプル コードは、どちらも Enumerable.Contains の呼び出し例を示しています。最初の例は、単純にリスト内に数字の 5 が含まれているかどうかを確認し、2 番目の例は、"Chai" に対応する要素が製品リスト内に存在するかどうかを確認します (この例では、前述のコードと同じ ProductComparer クラスを使用しています)。
' From ContainsDemo in the sample:
' Simple check to see if list contains a value:
Dim numbers = Enumerable.Range(1, 10)
Dim contains5 = numbers.Contains(5)

' More complex objects require more complex comparison:
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

Dim item As New Product _
  With {.ProductID = 1, .ProductName = "Chai"}
Dim containsChai = results.Contains(item, New ProductComparer())
2 つのシーケンスが等しいかどうかを確認するには、Enumerable.SequenceEqual メソッドを呼び出します。Enumerable.Contains メソッドの場合と同様、単純な値から成る 2 つのシーケンスを比較する場合は既定の比較処理を使用し、より複雑なオブジェクトから成る 2 つのシーケンスを比較する場合はカスタムの比較処理を使用します。
2 つのシーケンスに同じ型が含まれていない場合、または 2 つのシーケンスの長さが異なる場合、比較は直ちに失敗します。2 つのシーケンスに同じ型が含まれていて、2 つのシーケンスの長さが等しい場合、Enumerable.SequenceEqual メソッドは指定された比較処理を使用して各項目を比較します。図 7 は、Enumerable.SequenceEqual メソッドの両方の呼び出し方法を示しています。どちらの場合も戻り値は False です。
' From SequenceEqualDemo in sample:
Dim rnd As New Random
Dim start = rnd.Next

Dim count = 10
Dim s1 = Enumerable.Range(start, count)

' Choose a different random starting point:
start = rnd.Next
Dim s2 = Enumerable.Range(start, count)
Dim sequencesEqual = s1.SequenceEqual(s2)
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)

' What if there's no default comparer? Must use your own:
Dim products1 = _
  From prod In db.Products _
  Where prod.CategoryID = 1
Dim products2 = _
  From prod In products1 _
  Where prod.UnitPrice > 5

sequencesEqual = _
  products1.SequenceEqual(products2, New ProductComparer())
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)
次回のコラムでは、便利なセットベースの操作を行うおもしろいメソッドなど、Enumerable クラスのその他のメソッドに重点を置いて説明します。そのときまでに、サンプル アプリケーションをダウンロードして、Enumerable クラスのメソッドを試してみてください。Visual Studio® 2008 のデータ構造体を使用した処理は、それらのメソッドによって確実に強化されます。

ご質問やご意見は、Ken (basics@microsoft.com) まで英語でお送りください。

Ken Getz は、MCW Technologies のシニア コンサルタントであり、AppDev (www.appdev.com) のコースウェアの作成者でもあります。Ken は、『ASP.NET Developers Jumpstart』(Addison-Wesley、2002 年)、『Access Developer's Handbook』(Sybex、2002 年)、および『VBA Developer's Handbook, 2nd Edition』(Sybex、2001 年) の共著者です。連絡先は keng@mcwtech.com (英語) です。

Page view tracker