Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第37回 LINQの基本

2021/7/11

この記事が対象とする製品・バージョン

VB2019 Visual Basic 2019 ◎ 対象です。
VB2017 Visual Basic 2017 ◎ 対象です。
VB2015 Visual Basic 2015 ◎ 対象です。
VB2013 Visual Basic 2013 ◎ 対象です。
VB2012 Visual Basic 2012 ◎ 対象です。
VB2010 Visual Basic 2010 ◎ 対象です。
VB2008 Visual Basic 2008 ○ 対象ですが、使用できない機能も含んでいます。
VB2005 Visual Basic 2005 × 対象外です。
VB.NET 2003 Visual Basic.NET 2003 × 対象外です。
VB.NET 2002 Visual Basic.NET (2002) × 対象外です。
VB6対応 Visual Basic 6.0 × 対象外です。

 

目次

 

1.LINQ

1-1.言語統合クエリ

LINQ(読み方:LINQ=リンク)は、集合を操作する機能です。これを使うと合計や平均を求めたり、条件に合致するものを抽出したり、並び替えをしたり、いろいろな処理が記述できます。

LINQが集計の対象とするものを「データソース」と呼びます。具体的にはデータベースや、XMLや、ファイルシステムや、単なる配列やコレクションなど、プログラムに登場するさまざまなものをLINQのデータソースとすることが可能です。

次の例は、文字列の配列をデータソースとして、LINQで条件に合致する文字列だけを抽出します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim dataSource = {"徳川家康", "織田信長", "徳川吉宗", "豊臣秀吉", "徳川慶喜"}

'LINQで処理を定義。「徳川」から始まるものだけを抽出するクエリを作ります。
Dim results = From data In dataSource Where data.StartsWith("徳川")

'クエリを実行して結果を表示
results.ToList.ForEach(Sub(data) Debug.WriteLine(data))

→ Debug.WriteLineで出力される場所

真ん中の行の From name In Nams Where name.StartsWith("徳川") という部分が LINQ です。この独特な構文はLINQのクエリ式またはクエリ構文と呼ばれます。

SQLというデータベースを操作するための言語をモデルとしているため、VBよりもSQLに似ています。

 

次のプログラムはデータソースとしてAzure Cosmos DB というデータベースを使用します。

この例を実行するには事前にAzureと契約してCosmos DBデータベースを作成してデータを登録しておくなどいくつか準備が必要があるので、ここでは眺めるだけにしてください。

コメントで示しているLINQの部分は上述の配列をデータソースとして結果を抽出する例とほとんど同じになっていることがわかります。

VB2015 VB2017 VB2019

'データソース作成 (Cosmos DBに接続してデータを取得する処理を定義)
Dim cosmosClient As New CosmosClient("DefaultEndpointsProtocol=https;AccountName=xxxxxx;AccountKey=U2V0IHlvdXIgYWNjb3VudCBrZXkgZnJvbSBhenVyZSBwb3J0YWw=;TableEndpoint=https://xxxxxx.table.cosmos.azure.com:443/;")
Dim container As Container = cosmosClient.GetDatabase("MySampleDB").GetContainer("MySampleContainer")
Dim dataSource = container.GetItemLinqQueryable(Of Person)(True)

'LINQで処理を定義。Nameが「徳川」から始まるアイテムだけを抽出するクエリを作ります。
Dim results = From data In dataSource Where data.Name.StartsWith("徳川")

'クエリを実行して結果を表示
results.ToList.ForEach(Sub(data) Debug.WriteLine(data.Name))

→ Debug.WriteLineで出力される場所

 

データソースの取得方法はデータベースの種類や、ファイルなのかXMLなのか配列なのかで変わってしまいますが、その後の集計処理は同じように書けるというのがLINQの強みです。

同じように書けるといっても、データソースの性質によって書き方は変わるもので、上の2つの例でも配列の場合でも Where data.StartsWith("徳川") と改訂部分が、Cosmos DBの例では Where data.Name.StartsWith("徳川") のように「.Name」が増えています。

そして、この集計機能が、データベースの機能や、何かの外部のソフトウェアやサービスの機能なのではなく、Visual Basic の言語の機能として提供されている点がポイントです。LINQという名前も「言語統合クエリ」(Language-Integrated Query)の略称であり、この点が一番のポイントとして強調されているわけです。

VBの言語の一部ですから、スペルミスや何か記述に間違いがあればVBが指摘してくれますし、LINQの中から通所のプログラムと同じようにいろいろなクラスやメソッドを呼び出したり、変数を使ったりすることもできます。これは大きなメリットです。

メモ メモ  - クエリとは?

「クエリ」(query)とは問い合わせという意味です。何かを問い合わせて結果が返ってくる処理の場合、この問い合わせをクエリと呼びます。インターネットで「徳川家康」と検索する場合、この「徳川家康」がクエリです。LINQで「徳川」から始まるデータを取得したいという場合、そのLINQ式がクエリです。

 

操作方法 雑談 - 言語に統合されていないクエリとは?

言語に統合されていないクエリの代表は SQL です。データベースを集計するためのクエリ言語であるSQLは、VBの一部ではないため、VBで直接SQLを使用するときは次のように単なる文字列として記述する必要があります。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim cn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Database\Animals.mdb")
Dim query As OleDbCommand = cn.CreateCommand

'↓このSELECT ... の部分が言語に統合されていないクエリです。
query.CommandText = "SELECT 説明 FROM T_目マスタ WHERE 目ID = 2"

Dim Value As String

cn.Open()
Value = query.ExecuteScalar
cn.Close()

MsgBox(Value)

VBから見ると言語に統合されていない単なる文字列のため、この記述でタイピングのミスや、おかしな構文を使ってもVBは指摘してくれず、提案もしてくれません。また、この中でVBのクラスやメソッドを使用することも当然できません。

 

1-2.LINQプロバイダー

前述のようにLINQはデータベースやXMLやファイルシステムや、配列やコレクションを対象のデータソースにできます。

データソースによって内部でLINQを実行する仕組みは異なっており、LINQ機能を提供するものを「LINQプロバイダー」と呼びます。

LINQを使用するときに、LINQプロバイダーのことを意識することはあまりありませんし、このことを知らなくてもプログラムはできますが、ドキュメントなどを読んでいると出てくる言葉なので簡単に知っておくのが良いと思います。

ここで説明する内容にはあまりこだわらず次に進むことをお勧めします。

 

次の4つのLINQプロバイダーは2007年のLINQ登場当初からVisual Basic に含まれておりすぐに利用できます。

登場から10年以上経ち、この中の2つ(LINQ to SQL と LINQ to Dataset)は現在ではほとんど使われなくなっています。

LINQ to Objects

リンク トゥー オブジェクツ

配列やコレクションなど、IEnumerableインターフェース・IEnumerable(Of T)インターフェースを対象にします。

最も使いやすい一般的なLINQです。サンプルにもよく登場します。

Stringやファイルやフォルダーの配列など、プログラムに登場する多くのコレクションはIEnumerable(Of T)を実装しているのでLINQ to Objectsの対象です。

LINQ to SQL

リンク トゥー エスキューエル

今(2021年6月)ではほとんど使われません。LINQで記述した内容がSQL文としてデータベースで実行されるというコンセプトでした。
2021年6月時点ではデータベースを対象にLINQしたい場合は、LINQ to Entities や Cosmos DB への LINQ(※1) を使用します。

LINQ to XML

リンク トゥー エックスエムエル

XMLを対象にするときに非常に強力な力を発揮します。特にVBではXMLも言語機能と統合されているため、他のあらゆる言語の追随を許さないレベルで、XMLの制御を記述できます。

ただし、VBのXML言語機能が独特の構文になっており、使いこなすには少し学習が必要です。

LINQ to Dataset

リンク トゥー データセット

フレームワークのデータベース操作機能である ADO.NET で取得・生成したデータに対してLINQ機能を提供します。

ADO.NET を直接プログラムで利用する手法は 2003年ごろは主流だったのですが、その後さまざまなものが考案され、2021年6月現在では、ADO.NETを直接使用して複雑なアプリケーションを構築することが少なくなっています。そのため、LINQ to Dataset の出番もあまりありません。

 

LINQ to Objects を基本的に使えるようにしておけば、後は大体何とかなります。

 

この他に、追加のLINQプロバイダーもいろいろ、2007年のLINQ登場当初は LINQ to Google などいろいろ盛り上がりました。データや何かの集合を制御する方法を調べているとLINQが提供されていることがしばしばあります。

 

参考

LINQ の概要 - Visual Basic | Microsoft Docs

LINQ to Entities - ADO.NET | Microsoft Docs

 

1-3.ラムダ式との関係

上述のLINQのプログラムはLINQを使わないで次のようにプログラムしても同じ結果を得ることができます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim Names = {"徳川家康", "織田信長", "徳川吉宗", "豊臣秀吉", "徳川慶喜"}

'ラムダ式で「徳川」から始まるものだけを抽出する処理を定義
Dim results = Names.Where(Function(name) name.StartsWith("徳川"))

'ラムダ式を実行して結果を表示
results.ToList.ForEach(Sub(name) Debug.WriteLine(name))

実際この2つは、書き方が違うだけで全く同じ処理です。LINQのクエリ式は内部では処理をラムダ式に翻訳して実行しています。

そのためどちらの構文を使うかはまったくのお好みですが、ラムダ式を使う構文だと Sub(xxx) や Function(xxx) を書く必要があるため、複雑になってくるほど見にくくなり、クエリ式を使った方が簡潔に書けることが多いようです。

メモ メモ  - LINQ の示す範囲

メソッドチェーンで合計や抽出を行う操作も LINQ 機能であるとする場合もあります。この場合、これを LINQ のメソッド構文と読んだりするようです。構文が違うだけで同じ機能だからです。

このあたりのVBの機能については、マイクロソフトの用語の定義や使い方がはっきりしていないように感じるで、私もどこまでをLINQと呼ぶのか迷いがあります。下記のドキュメントではどうもクエリ構文の場合だけを指して LINQ と呼んでいるように解釈するのが自然なようなので、私のこの初級講座の記事でもクエリ構文だけをLINQと呼ぶことにします。

LINQ の概要 - Visual Basic | Microsoft Docs

 

2.LINQの構文

2-1.LINQの構文

LINQには様々な機能があります。2009年に出版された「プログラミング LINQ」という書籍が700ページ以上ある大著であることからもそのボリュームがわかります。

この記事では、LINQ to Objects を使って、LINQの基本がわかる機能や構文を説明していきます。深く学習したい場合は、Microsoft Docsなどを利用されることをお勧めします。(が、先頭から呼んでいければ習得できるという形式ではないので苦労します。)

LINQ の概要 - Visual Basic | Microsoft Docs

 

2-2.最小のLINQ

まず、もっとも小さいLINQを紹介しましょう。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals = {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"}

'LINQで処理を定義
Dim results = From animal In animals

'クエリを実行して結果を表示
results.ToList.ForEach(Sub(animal) Debug.WriteLine(animal))

→ Debug.WriteLineで出力される場所

このプログラムはデータソースに対して何も処理をしないでそのまま結果を生成します。

ですから、練習用に使う以外には意味はありません。実行すると「アメンボ」、「イノシシ」、「ウマ」、「エリマキトカゲ」、「オオカミ」が出力されます。

LINQの部分だけ抜き出すと次のようになっています。


From animal In animals

このFrom ~ In ~(読み方:From = フロム、In = イン)というのはLINQでもっともよく使われるパーツです。このFrom ~ In ~の部分のことを「From句」と呼びます。

Fromの後ろにはデータソースの要素を表現するための変数を指定し、Inの後ろにはデータソースを指定します。

「データソースの要素を表現するための変数」のことを「反復変数」と呼びます。上記の例ではanimalが反復変数です。反復変数はLINQの中だけで使用できます。

この部分を構文風に表現すると次のようになります。


From 反復変数 In データソース

データソース

データソースには、既に説明したように配列やコレクションやデータベースのデータやXML、ファイルシステムの情報などさまざまなものを指定できます。背景でラムダ式が動作していることからわかるかもしれませんが、IEnumerable(Of T)インターフェースまたはIEnumerableインターフェースを実装しているクラスはすべてデータソースとして使用できますが、それ以外のものでもデータソースに指定できるものもあります。(機能ごとにLINQに個別対応しているため、データソースに指定できるものを汎用的に示す仕様はありません。)

反復変数

反復変数の型は上記の例のようにString型の配列がデータソースであればStringと推論されます。必要であれば自分で As String を付けて型を明示することもできます。

この最もシンプルなLINQでは、反復変数には何の役割もありませんが、省略することはできません。複雑なクエリを記述する場合にこの反復変数を利用していくことになります。

 

2-3.LINQの戻り値

LINQの戻り値は何かのコレクションであったり、単一の数値であったりさまざまです。

LINQでどのような操作を行うかで戻り値は変わります。

 

3.Where

3-1.構文と使用例

Whereを使うと抽出条件を指定することができます。


From 反復変数 In データソース Where 条件

条件は、If文のように記述します。ほとんどの場合、条件を記述するのに反復変数を使用します。

たとえば、4文字のものだけを抽出するには次のようにします。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals = {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"}

'LINQで処理を定義
Dim results = From animal In animals Where animal.Length = 4

'クエリを実行して結果を表示
results.ToList.ForEach(Sub(animal) Debug.WriteLine(animal))

→ Debug.WriteLineで出力される場所

実行すると「アメンボ」、「イノシシ」、「オオカミ」が出力されます。

 

3-2.VBの言語機能との組み合わせ

言語統合クエリなので VBの機能はすべて使用できます。

たとえば、部分一致を指定するVBのLIKE演算子を使用すると、「マ」が含まれるものだけを抽出することも簡単にできます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals = {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"}

'LINQで処理を定義
Dim results = From animal In animals Where animal LIKE "*マ*"

'クエリを実行して結果を表示
results.ToList.ForEach(Sub(animal) Debug.WriteLine(animal))

→ Debug.WriteLineで出力される場所

実行すると「ウマ」、「エリマキトカゲ」が出力されます。

 

3-3.メソッドの呼び出し

Where句の中から別途定義しているメソッドや関数を呼び出すこともできます。とにかく普通のVBでできることは記述できます。

次に例では Where句の 中で別途定義しているメソッド GetAnimalClass を呼び出しています。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub LINQTest()

    'データソース作成
    Dim animals = {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"}

    'LINQで処理を定義
    Dim results = From animal In animals
                  Where GetAnimalClass(animal) = "哺乳綱"

    'クエリを実行して結果を表示
    results.ToList.ForEach(Sub(animal) Debug.WriteLine(animal))

End Sub

''' <summary>動物分類上の「綱」の名前を取得します。</summary>
Private Function GetAnimalClass(animalName As String) As String

    Dim animalClass As String = ""

    Select Case animalName
        Case "アメンボ"
            animalClass = "昆虫綱" '節足動物門
        Case "イノシシ"
            animalClass = "哺乳綱" '脊索動物門
        Case "ウマ"
            animalClass = "哺乳綱" '脊索動物門
        Case "エリマキトカゲ"
            animalClass = "爬虫綱" '脊索動物門
        Case "オオカミ"
            animalClass = "哺乳綱" '脊索動物門
    End Select

    Return animalClass

End Function

→ Debug.WriteLineで出力される場所

この例では、LINQを2行に分けて、2行目に Where 句を書いています。LINQが長くなってくると見にくくなってくるのでこのように適宜改行することが通常行われます。

プログラム中で記号なしで改行できるようになったのは VB2010 からなので、この例は VB2010 以上でないと実行できません。

 

4.Select

4-1.構文と使用例

Select句を使うと結果として生成するオブジェクトが持つプロパティを定義できます。


From 反復変数 In データソース Where 条件 
Select [プロパティ名1 =] 式1, [プロパティ名2 =] 式2,...

Where句など他の句と一緒に使用することもできますし、単独で使用することもできます。(Fron ... In ... は必須です。)

Select句が役に立つ場合の多くは反復変数がStringやIntegerなどのプリミティブ型ではなく、多くのプロパティや項目をもつオブジェクトの場合です。

次の例は Windowsフォルダーから、5つのファイルを取得し、その名前とサイズだけを結果として取得します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'Windowsフォルダー(例 C:\windows)のファイルの一覧
Dim winFolder As New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows))
Dim fileList As IEnumerable(Of IO.FileInfo) = winFolder.EnumerateFiles

'ファイルの名前とサイズを抽出
Dim files = From file As IO.FileInfo In fileList
            Select file.Name, file.Length

'file は Nameプロパティ と Length プロパティだけを持つ匿名型です。
For Each file In files
    Debug.WriteLine(file.Name & " " & file.Length.ToString)
Next

→ Debug.WriteLineで出力される場所

結果は次のように出力されます。

bfsvc.exe 77824
bootstat.dat 67584
comsetup.log 762
diagerr.xml 7623
diagwrn.xml 7623

この例ではデータソースが FileInfoのコレクションなので、反復変数 file の型は  FileInfo 型になります。Select句がなければ、LINQの結果もFileInfo型のコレクションになるのですが、Select句で NameプロパティとLengthプロパティの2つだけが指定されているため、結果に含まれるのは この2つのプロパティだけになります。

そのため、結果を表す変数 files の型は匿名型のコレクションになります。

 

4-2.プロパティに名前を付ける

結果に含まれるプロパティの名前を指定することもできます。Lengthプロパティはバイト単位でのサイズを表しているので 計算して キロバイト単位のサイズを表すようにプログラムを改造してみましょう。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'Windowsフォルダー(例 C:\windows)のファイルの一覧
Dim winFolder As New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows))
Dim fileList As IEnumerable(Of IO.FileInfo) = winFolder.EnumerateFiles

'ファイルの名前とサイズを抽出
Dim files = From file As IO.FileInfo In fileList
            Select file.Name, KB = file.Length / 1024

'file は Nameプロパティ と KB プロパティだけを持つ匿名型です。
For Each file In files
    Debug.WriteLine(file.Name & " " & file.KB.ToString("0.0 KB"))
Next

このように Select句の中で プロパティ名 = と記述すると好きなプロパティ名を結果に含ませることができるようになります。

結果は Nameプロパティと KBプロパティを持つ匿名オブジェクトの集合になります。

この例を実行すると次のように出力されます。

bfsvc.exe 38.5 KB
bootstat.dat 33.4 KB
comsetup.log 0.4 KB
diagerr.xml 3.8 KB
diagwrn.xml 3.8 KB

 

4-3.何が起こっているのか

もう少しSelectを詳しく見てみます。

Whereや今後説明する Order By や Distinct などのLINQの機能は基となるデータソースから、対象を絞り込んだり並び変えたりする機能です。

一方、Select は、基となるデータソースとは異なるオブジェクトを生成するという点で特徴的です。

メモ メモ  - 射影

この特徴をとらえてSelectの機能を「データソースの射影を作成する。」と表現したり、「データソースをマッピングする」と表現する場合もあります。

Microsoft DocsではSelectを射影操作と分類しています。

射影操作 - Visual Basic | Microsoft Docs

 

つまり、文字列のコレクションをどんなにWhereしたり、Order Byしたりしても結果は、文字列のコレクションであるという点では変わりません。

しかし、Selectした結果は、文字列のコレクションではないかもしれません。

たとえば、次の例では文字列のコレクションをデータソースにして、数値型のコレクションを生成します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals = {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"}

'LINQで処理を定義
Dim results As IEnumerable(Of Integer) = From animal In animals Select Len(animal)

このデータソースから、結果を得るための変換処理を定義するのが Select の役割です。

 

次のような無意味な変換も定義できます。

この例だと、Selectには 3.14 という固定値が記述されているため、データソースの項目が何であれ、3.14 に変換します。そのため、結果セットは 3.14 を 5つ含むコレクションになります。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals = {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"}

'LINQで処理を定義
Dim results As IEnumerable(Of Double) = From animal In animals Select 3.14

 

Selectは文字列や数値のようなプリミティブ型だけではなく、匿名オブジェクトを生成することもできます。

この例では 文字列の最初の1文字と、文字列の長さの組み合わせをプロパティとして持つオブジェクトがSelect句によって生成されます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals = {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"}

'LINQで処理を定義
Dim results = From animal In animals Select animal.First, animal.Length

'3番目の要素を取り出して、値を確認してみます。
Dim item3 = results(2)
Debug.WriteLine("First = " & item3.First)
Debug.WriteLine("Length = " & item3.Length)

このオブジェクトは事前に定義されているものではないので、匿名オブジェクトとなります。そのため、この例では変数 results に As を使って実際の型を明示することができません。型推論機能を使うしかないということです。

 

5.Let

Let (読み方:Let=レット)を使うとLINQの中で使用できる変数を定義することができます。

たとえば、先ほどのWindowsフォルダーにあるファイルのサイズをKB単位で取得する例を改造して、サイズが10KB以上のものだけを抽出するようにしてみます。Letを使わない場合、次のような記述になります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'Windowsフォルダー(例 C:\windows)のファイルの一覧
Dim winFolder As New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows))
Dim fileList As IEnumerable(Of IO.FileInfo) = winFolder.EnumerateFiles

'ファイルの名前とサイズを抽出
Dim files = From file As IO.FileInfo In fileList
            Where file.Length / 1024 > 10
            Select file.Name, KB = file.Length / 1024

'file は Nameプロパティ と KB プロパティだけを持つ匿名型です。
For Each file In files
    Debug.WriteLine(file.Name & " " & file.KB.ToString("0.0 KB"))
Next

この LINQ には file.Length / 1024 という計算が2回登場しており非効率です。

Let を使うと file.Length / 1024 の値は1回計算したもの変数にとっておいて後で使いまわすことできます。

次のようになります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'Windowsフォルダー(例 C:\windows)のファイルの一覧
Dim winFolder As New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows))
Dim fileList As IEnumerable(Of IO.FileInfo) = winFolder.EnumerateFiles

'ファイルの名前とサイズを抽出
Dim files = From file As IO.FileInfo In fileList
            Let KB = file.Length / 1024
            Where KB > 10
            Select file.Name, KB

'file は Nameプロパティ と KB プロパティだけを持つ匿名型です。
For Each file In files
    Debug.WriteLine(file.Name & " " & file.KB.ToString("0.0 KB"))
Next

初めの例よりずっとすっきりします。

Let句内ではカンマで区切って複数の変数を定義することもできます。

 

6.遅延実行

6-1.値を取り出すときに実行される

LINQはLINQを記述している行で実行されるのではなく、値を取り出すときに実行されます。

さきほどWhereの例で登場したのと似た 4文字の要素を抽出するLINQを題材に眺めてみます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals As New List(Of String)({"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"})

'LINQで処理を定義
Dim results = From animal In animals Where animal.Length = 4

'クエリを実行して結果を表示
results.ToList.ForEach(Sub(animal) Debug.WriteLine(animal))

Dim results = From animal … の行では LINQ は実行されません。

ここで定義されているLINQが実行されるのは、最後の行で ToList メソッドを実行するときです。なぜならば、ToListメソッドは、コレクションをList(Of T)に変換する機能なので、この時点でコレクションの内容が確定していないと処理できないからです。なので、このタイミングでLINQが実行されて、戻り値 results の内容が確定します。

 

そのため、LINQを記述している行より下で、データソースを変更すると、LINQはちゃんと変更後のデータソースを使用します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals As New List(Of String)({"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"})

'LINQで処理を定義
Dim results = From animal In animals Where animal.Length = 4

'データソースに要素を追加
animals.Add("カマキリ")

'クエリを実行して結果を表示
results.ToList.ForEach(Sub(animal) Debug.WriteLine(animal))

この例では、表示される結果に「カマキリ」が追加されます。

 

ToListが実行される前にブレイクポイントで実行を停止して、results の内容を見てみるとどう見えるでしょうか?

results.Add("カマキリ")の行にブレークポイントを設定して、実行を停止してみましょう。result.Add("カマキリ")の行が黄色く表示されます。これは、まだこの行が実行されていないことを示します。

変数 resultsの上でマウスをホバーされると、次のようなヒントが表示されます。

この表示はVisual Studioのバージョンによって異なります。ここではVisual Studio 2019を例に取り上げます。

ここにはLINQの結果が表示されていません。まだ、LINQが実行されていないからです。

その代わり「Results View」(結果ビュー)という項目があり、説明には英語で「Expanding the Results View will enumerate the IEnumerable」と書いてあります。これは、「Results Viewを展開すると、IEnumerableを列挙します。」という意味です。少しわかりにくいのですが、ここを 展開するとするとLINQを実行するぞ ということです。

Visual Studio 2019ではマウスをホバーさせると展開されます。展開すると、この時点でLINQが実行され「アメンボ」、「イノシシ」、「オオカミ」が表示されます。

 

この例では ToList で値を取り出していますが、For Each などさまざまな他の手段を使っていても、それぞれ値を必要になった時にLINQは実行されます。

 

6-2.LINQの重ねがけ

LINQはLINQを記述している行で実行されるわけではないので、プログラムはちょっとずつ記述しても結果は同じです。

次の例ではLINQの結果にLINQを適用し、さらにその結果にLINQを適用しています。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals As New List(Of String)({"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"})

'LINQで処理を定義
Dim results = From animal In animals Where animal.Length = 4

results = From animal In results Select animal & " です。"

results = From animal In results Order By animal Descending

まだ説明していない Order By ... Descending が登場しています。これは結果を逆順で並べるという指示です。

このLINQは1つの記述にまとめることができて同じ意味です。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals As New List(Of String)({"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"})

'LINQで処理を定義
Dim results = From animal In animals Where animal.Length = 4
              Select newName = animal & " です。"
              Order By newName Descending

いっけん、一つにまとめたほうが実行効率が良いように感じるかもしれませんが、3つに分けても実行効率は変わりません。値が必要になった時に実行されるので、実行されるときにはどちらも同じ処理として定義されている状態だからです。

 

6-3.即時実行

何かの事情ですぐにLINQを実行したい場合は、ToList や ToArray メソッドなどを使って、LINQの結果を別のコレクションに変換するように指示します。さきほどのToListのところで説明したように、この操作を実行するには、LINQの結果が確定している必要があるため、この時点でLINQが実行されます。

さて、次のプログラムを実行した時に結果に「カマキリ」は表示されるでしょうか?

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'データソース作成
Dim animals As New List(Of String)({"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"})

'LINQで処理を定義
Dim results = (From animal In animals Where animal.Length = 4).ToList

'データソースに要素を追加
animals.Add("カマキリ")

'クエリを実行して結果を表示
results.ToList.ForEach(Sub(animal) Debug.WriteLine(animal))

答えは、「いいえ、カマキリは表示されません。」です。

LINQの行に ToList があるため、この時点でLINQが実行されます。カマキリはLINQが実行された後で追加されるので、もはや結果に含まれません。

 

なお、次回説明予定の Aggregate を使用するLINQは何もしなくても即時実行されます。