連載! とことん VB: 第 23 回 LINQ の基本構文と、その背後に潜む LINQ の「からくり」

更新日: 2010 年 5 月 21 日

執筆者: エディフィストラーニング株式会社 矢嶋 聡

この記事は、「MSDN プログラミング シリーズ」として発行している技術書籍「ステップアップ Visual Basic 2010 ~開発者がもう一歩上達するための必読アドバイス」(日経 BP 社刊) を基に先進的なテクニックを紹介しています。


目次

  1. はじめに
  2. 基本的なクエリ式
  3. クエリ結果(クエリ変数)の型
  4. 匿名型の利用
  5. 標準クエリ演算子の直接的な呼び出し

1. はじめに

この一連の記事では、統合言語クエリ (LINQ: Language-Integrated Query) の理解を深めるための関連知識として、ジェネリックや拡張メソッド、ラムダ式について取り上げました。そして、今回から 4 回に渡り、いよいよ、これまで説明した知識をベースとして、LINQ の構文や関連するライブラリについて解説します。(これまでに説明した「型推論」、「匿名型」、「拡張メソッド」、「ラムダ式」などの概念を理解した上で、読み進めてください。)

そもそも、LINQ とは様々な種類の異なるデータに一様な方法でアクセスするためのテクノロジです。その実体は、Visual Basic の構文やライブラリとして提供されます。これらの中で、今回は、構文としての基本事項を改めて振り返り、それに関連する注意点などについて取り上げます。

今後は LINQ を使う機会もますます増えて来ることでしょう。今のうちに、しっかりマスターしておきましょう。

 ページのトップへ


2. 基本的なクエリ式

まず、データソースに対してクエリを行う「クエリ式」の書き方について取り上げます。基本的なクエリ式にも様々なバリエーションがあり、一部を見落としてしまうことがあるかも知れません。この点もふまえ、基本的な構文を改めて確認してみましょう。

次の例 1 は基本的なクエリ式を使用した例です。(このサンプルコードを実行するには、Windows フォームアプリケーションを作成し、フォームに 1 つのボタンと 1 つのリストボックスを貼り付けた後、このサンプルコードと同じになるよう修正や追加を行ってください。)

例 1. 基本的なクエリ式

Public Class Form1

     Private Sub Button1_Click(...) Handles Button1.Click ←Clickイベントハンドラ

          Dim data() As Integer = {200, 500, 100, 300, 600, 400} ←[1]

          Dim query1 = From d In data Where d <= 300 Select d ←[2]
          For Each d As Integer In query1 ←[3]
               ListBox1.Items.Add(d)
          Next

     End Sub

End Class

ここでは簡単にするため、[1] に定義された Integer 型の配列 data に対して、クエリ操作を行います。[2] の太字部分が、この配列に対してクエリ操作を行うクエリ式です。ここでは、値が 300 以下の要素を抽出しています。クエリ式は、文字通り「抽出結果という値を求める式」であり、式自体が値を表すので、[2] にあるように変数 (ここでは変数 query1) に代入できます。

このクエリ式が返す結果を代入した変数は「クエリ変数」と呼ばれています。論理的には、クエリ変数はクエリ結果 (抽出結果の要素集合) とみなすことができます。しかし、厳密にいうと、実は、クエリ変数にクエリ式を代入しただけでは (上記の [2] を実行しただけでは)、実際のクエリ操作は実行されていません。いつ実行されるかは、データソースやクエリ式の書き方に依存しますが、一般に LINQ では実際に要素を参照するまではクエリ操作が保留されます。これを「遅延実行」と呼びます。この例でも、クエリの書き方によって実行のタイミングが若干異なるのですが、少なくも [3] の For Each ループを使用してクエリ結果 (クエリ変数) から実際に要素を参照するまでは、クエリが実行されないと考えておいてください。

また Visual Basic では、原則として 1 つの式や 1 つのステートメントを書くとき、途中で改行せずに 1 行に書く必要がありますが (改行をおこなうには _ (アンダーバー) を使用する必要がありますが)、Visual Basic 2010 の新機能として、行の継続が想定されるいくつかの構文では、途中改行が認められるようになりました。よって、次の例 2 のようにクエリ式を 2 行に渡って記述しても有効な構文となります (1 行目の末尾に継続を表す記号である「アンダーバー」を書く必要はありません)。特にクエリ式は長くなりがちです。このような自然な改行によって、可読性も向上するでしょう。

例 2. クエリ式の暗黙的な行継続 (Visual Basic 2010 から)

    Dim query1 =  From d In data
                    Where d <= 300 Select d

次に、このクエリ式の意味を考えてみましょう。

クエリ操作の対象となるデータソース (データの集合) は、In キーワードの後ろに記述されます。また、その集合の要素 1 つを表す変数定義が d になります。よって、次のような For Each ループと同じ構造と考えてよいでしょう。

Visual Basic
For Each d In data
 

この変数 d はクエリ式の中だけで利用されるローカル変数であり、Where キーワード以降で使用されています。そして、Where 句には抽出条件を書き (ここでは要素 d が 300 以下)、Select 句には抽出した要素の形式を指定します。

Select 句では上記の例のように単に「Select d」と書けば、元の要素の形のままとなり、抽出結果は、200、100、および 300 という値の集合となります。ただし、構文として有効な式であれば何でも書けるので、次のように書くことも可能です。

Visual Basic
Select d + 1Select CStr(d)
 

前者では、最終的なクエリ結果は、各要素に 1 が加算され、201、101、301 となります。後者は、各要素が String 型に変換されます。

なお、例 1 や例 2 のように「Select d」と書く場合は、抽出結果の要素は特に加工しないので、次のように Select 句を省略しても同じです。このような省略もよくあるので、書き方に慣れておいてください。(実は、厳密にいうと、この例 3 の省略形のほうが、例 1 や例 2 と比較して、オーバーヘッドが少なくなる場合があるのです。これについては、後述します。)

例 3. Select 句の省略

    Dim query1 =  From d In data Where d <= 300

 ページのトップへ


3. クエリ結果(クエリ変数)の型

クエリ変数 (例 1 の変数 query1) の型は、IEnumerable(Of T) 型 (または、その派生型) であり、コレクションを表すインターフェイスです。実際は、使用するクエリ式やデータソース (今回の場合、データソースは整数の集合です) に応じて呼び出されるライブラリが異なるため、これによって型が決まります。しかし、コンパイラが型推論によって型を判別できるので、明示的に As 句を書く必要もなく、クエリ変数の型のことで悩むことはないでしょう。

また、既に触れたように、クエリ変数はクエリ結果の要素の "論理的なコレクション" であり、型パラメータ T の部分は、要素の型になります。例 1 の [2] のクエリ式であれば、Integer 型の要素を返すので、クエリ変数は IEnumerable(Of Integer) 型です。

型パラメータ T の部分は、Select 句に記述された要素の形式に依存します。よって、Select 句の書き方によってクエリ変数の型が異なるので、同じデータソースに対するクエリ式であっも、同一のクエリ変数の使い回しが出来るとは限らないので注意してください。

例えば以下の例 4 では、3 つのクエリ式を同一のクエリ変数に代入する簡単な実験をしています。

例 4. Select 句による型の違い

Dim query1 = From d In data Where d <= 300 ←[1]
query1 = From d In data Where d > 100 Select d ←[2]
query1 = From d In data Where d > 100 Select String.Format("{0}", d) ←[3]

上記では、[1] で変数を定義して、この時点で型推論によってクエリ変数 query1 の型が決まり、Integer 型の要素を返すので、クエリ結果は IEnumerable(Of Integer) 型です。同様に [2] も、Integer 型要素を返すので、同一のクエリ変数 query1 を流用できます。しかし、[3] は要素を文字列に整形しているので、IEnumerable(Of String) です。既定の設定ではコンパイルオプションが Option Strict Off なので [3] はコンパイルエラーとして発覚しませんが、これは実行時にエラーになります。(Option Strict On に設定すると、[3] では型が合わずコンパイルエラーになります。)

なお、Select 句の典型的な書き方の中には、コンパイラによって新しい型 (匿名型) が自動生成され、この新しい型が IEnumerable(Of T) の型パラメータ T の部分になる場合もあります。この点を次に確認しましょう。

 ページのトップへ


4. 匿名型の利用

次の例では、Product オブジェクトのコレクションから、価格が 200 円未満の Product オブジェクトを抽出する例です。

例 5. Select 句での匿名型の利用

Public Class Form1

     Private Sub Button1_Click(...) Handles Button1.Click ←Clickイベントハンドラ

          Dim products As New List(Of Product) From ←[1]
               {New Product(100, "Orange", 150D),
               New Product(200, "Peach", 200D),
               New Product(300, "Apple", 100D),
               New Product(400, "Banana", 120D),
               New Product(500, "PineApple", 300D)}

          Dim query2 = From p In products Where p.Price < 200D ←[2]
                              Select p.ProductID, p.ProductName
          For Each p In query2 ←[3]
               ListBox1.Items.Add(p.ProductID & " " & p.ProductName) ←[4]
          Next

     End Sub

End Class

Public Class Product ←[5]
     Public Property ProductID As Integer
     Public Property ProductName As String
     Public Property Price As Decimal
     Public Sub New(ByVal id As Integer, ByVal nm As String, ByVal pr As Decimal)
          ProductID = id
          ProductName = nm
          Price = pr
     End Sub
End Class

[2] の 2 行の太字部分がクエリ式です。In キーワードの後ろに記述されたクエリ対象となるデータソースproducts は List(Of Product) 型のコレクションであり、要素は Product オブジェクトです。よって、クエリ式 [2] の From キーワードの後に定義された要素を表す変数pは、1 つの Product オブジェクトを表す Product 型の変数となります。同様に、Where でも Product 型の変数となります。

仮に Select 句の記述を省略すれば、抽出されたものは加工されないので、1 つ 1 つの要素の姿は Product オブジェクトのままになり、単に抽出条件を満たす Product オブジェクトの集合になります。しかし、上記の Select 句では、抽出した要素である各 Product オブジェクトを加工し、2 つのプロパティ (p.ProductIDとp.ProductName) を並べているので、抽出後の各要素は文字通り、ProductID と ProductName の 2 つのプロパティからなるオブジェクトになります。

当然ながら、このような 2 つのプロパティのオブジェクトの型の定義 (クラス定義) はどこにも存在しないので、コンパイラが型の定義を自動生成します。このような型は「匿名型」といいます。

この匿名型の名前は明示的にソースコード上に書くことはできませんが、第 3 回でも触れたように、型推論によって、[2] のクエリ変数 query2 の型は IEnumerable(Of 匿名型) になります。また他の部分でも、型の名前を明記できなくとも問題はなく、[3] の For Each ループにおいて抽出結果を参照する場合も、コンパイラによって p はこの匿名型と判断されます。さらに、[4] の "p.ProductID" のように、この匿名型のプロパティを参照できます。

なお、匿名型のプロパティ名を任意の名前に変更することもできます。次のように「名前 = 値」という形式で記述すると、元の ProductID プロパティと ProductName プロパティの値のままで、名前はそれぞれ ID プロパティ、Name プロパティになります。

Visual Basic
Select ID = p.ProductID, Name = p.ProductName
 

なお、Visual Basic 2008 からは、New キーワードおよび With キーワードを使用して明示的に匿名型を作成する構文が用意されています。それを利用すると、前述の 2 つの Select 句は次のように記述することもできます。

Visual Basic
Select New With {p.ProductID, p.ProductName} 
Select New With { .ID = p.ProductID, .Name = p.ProductName}
 

後者の場合は、ID プロパティと Name プロパティの前に、ドットを付ける必要があるので注意してください。

 ページのトップへ


5. 標準クエリ演算子の直接的な呼び出し

「第 21 回 ラムダ式の基本とその必要性」で説明した Where メソッドを思い出してください。コンパイラは、プログラム上にクエリ式を見つけると、LINQ 関連のライブラリに定義された相応のメソッド呼び出しに変換します。例えば、Where 句であれば Where メソッドの呼び出し、Select 句であれば Select メソッドの呼び出しという具合です。このようなメソッドは、「標準クエリ演算子」と呼ばれています。上記のようなクエリ構文を使用せず、この標準クエリ演算子を直接呼び出すコードを書くことも可能です。

クエリ構文のほうが書き方としては簡単ですが、クエリ式の構文はライブラリに定義された標準クエリ演算子の呼び出しを網羅しているわけではないので、場合によっては、標準クエリ演算子を直接呼び出すコードを書くこともあります。実際、一部のサンプルコードでは、標準クエリ演算子を使用している例も見受けられます。また、ドキュメントに記載された標準クエリ演算子の定義が理解できるようになれば、より詳しく LINQ の動作が理解できるようになります。さらに、クエリ式が標準クエリ演算子の呼び出しにどう対応するかを知ることによって、クエリ式の最適化のヒントになることもあります。

ここで、基本的なクエリ演算子の使用例を見てみましょう。次の例 6 は例 5 の products コレクションに対するクエリ式と、それと同等の標準クエリ演算子の呼び出しを比較した例です。

例 6. クエリ式と標準クエリ演算子

     Dim queryA = From p In products Where p.Price < 200D Select p
     Dim queryB = products.Where(Function(p) p.Price < 200D).Select(Function(p) p)

Visual Basic 2008 に慣れていないプログラマーの方は、後者のメソッド呼び出し形式のほうが、前者のクエリ式で行っていることが想像しやすいことでしょう。後者では、クエリ式に記述された Where 句、Select 句の順に、「メソッド」として呼び出しています。ここでは、Where メソッドを呼び出した結果の戻り値に対して、Select メソッドを呼び出すことになります。これらは、第 9 回 にも説明した「拡張メソッド」として Visual Basic に追加 (拡張) されているメソッドです。

Where メソッドの引数には、上述の通り、クエリ式の Where 句の抽出条件が「ラムダ式」として渡ります。このラムダ式では、引数として渡された要素に関して、このラムダ式の抽出条件 "p.Price < 200D" を評価することになります。(ラムダ式については、「第 21 回 ラムダ式の基本とその必要性」を参照してください。)

この Where メソッドの定義は、第 18 回や第 21 回にも触れた以下のジェネリック メソッドです。今回は型パラメータ TSource の部分は Product なので、Where メソッドの戻り値 (下記の太字部分) は、IEnumerable(Of Product) 型です。

例 7. (再掲) Where メソッドの定義

_
Public Shared Function Where(Of TSource) ( _
     source As IEnumerable(Of TSource), _
     predicate As Func(Of TSource, Boolean) _
) As IEnumerable(Of TSource)

さらに、この Where メソッドの戻り値の集合に関して、例 6 では Select メソッドを呼び出して、各要素を加工しています。加工方法 (Select 句での記述内容) は、Select メソッドの引数として、ラムダ式を渡します。仮に、例 5 の [2] のクエリ式のように、プロパティを 2 つ列挙して匿名型のオブジェクトを作るなら、Select メソッドのラムダ式は次の太字部分のようになります。

例 8. 匿名型のオブジェクトして返す

     Dim queryD = products.Where(Function(p) p.Price < 200D) _
                         .Select(Function(p) New With {p.ProductID, p.ProductName})

さてここで、例 6 の Select メソッドの呼び出しを見直してください。結局、この例 6 の Select メソッドでは要素に対して何も加工していないので、実は、Select メソッドの呼び出しを行う必要がありません。また、Select メソッドの呼び出しを記述しなければ、引数で渡された "Function(p) p" というラムダ式が、コンパイラによって無駄に匿名メソッドとして生成されることも無くなるということが読み取れるでしょう。

このように、クエリ式での各句 (Where 句や Select 句) とメソッド (標準クエリ演算子) の対応関係を把握しておけば、クエリ構文においても無駄を省くように調整できます。

以上、今回は基本的なクエリ式にまつわる留意点を取り上げました。これらを理解しておけば、プログラムの中で、実際に LINQ のクエリがどう作用するか理解も深まり、より使いこなせるようになることでしょう。


你可能感兴趣的:(VB.net,Linq&lambda)