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

第36回 集合の操作

2021/6/27

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

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.単純な集計

1-1.Sum

プログラムでは、たくさんあるものを集計するという処理がよく必要になります。

単純に、合計や平均求めたり、数を数えたりする処理もありますし、条件に合致するものを抽出したり、並び替えを行ったり、2つの集合を比較して、両方に存在するものだけを抜き取ったりいろいろな処理があります。

VBでは、ラムダ式を使ってこのような処理を簡単にプログラムすることができます。さらに、LINQのクエリ構文(読み方:LINQ=リンク)という特別な構文も用意されていて、使い慣れると快適です。LINQのクエリ構文については次回説明する予定です。

次の例は、数字の合計を求めます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
Dim amount = numbers.Sum
メモ メモ  - 配列リテラル

忘れている人のためにフォローすると {1,2,3,…} の部分は配列リテラルです。つまり、変数 numbers は Integerの配列です。VBでは { } を使って簡単に配列を記述することができ、これを配列リテラルと呼びます。

配列の型に限定はなく、{"A", "B", "C"} などのようにして文字列型の配列を作成することもできます。

これと型推論機能を組み合わせると、上述の例のようになります。

もっと簡略化してこんな書き方もできます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Dim amount = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}.Sum

この Sum メソッド(読み方:Sum=サム)はIEnumerableインターフェースの拡張メソッドです。使いやすいように配列やコレクションなど集合を表現するクラスは IEnumerableインターフェースを実装するように設計されており、Sum や 平均値を求める Average (読み方:Average=アベレージ)などは共通して呼び出せるようになっています。

だから、型がList(Of Integer)でも、同じようなことができます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim numbers As New List(Of Integer)({1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Dim amount = numbers.Sum

インターフェースと拡張メソッドをうまく組み合わせることで、型が違っても同じようなプログラムを書くことができてすばらしいですね。

 

1-2.Dictionaryの集計

もうちょっと複雑なクラスである Dictionary(Of String, Integer)ではどうでしょうか?

中間テストの科目と点数の情報から合計点を計算する方法を考えてみましょう。次のプログラムはエラーになります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim dic As New Dictionary(Of String, Integer) From {{"国語", 75}, {"数学", 88}, {"英語", 52}}
Dim acount = dic.Sum '←ここでエラーです。

私たちはテストの点数とわかれば、何も言われなくても 75 + 88 + 52 を計算すればよいとわかりますが、コンピューターには単なる文字と数字の組み合わせが3つあるとしか認識されていないので、これの合計をどういう処理で計算すればよいのかわからないのです。

Sumメソッドでは、ラムダ式を使って合計を計算する処理を指定することができます。

インテリセンスを見てどのようなラムダ式を指定すればよいのか調べてみましょう。

Sum( まで入力すると次のようなヒントが表示されます。

ちょっと複雑で見にくいですが、分解して理解すると次のことがわかります。

以上の情報から、構文は次のようなラムダ式が当てはまることがわかります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim dic As New Dictionary(Of String, Integer) From {{"国語", 75}, {"数学", 88}, {"英語", 52}}
Dim amount = dic.Sum(Function(param As KeyValuePair(Of String, Integer)) As Integer
                         Return 1
                     End Function)

このラムダ式はコレクションの要素の数だけ呼び出されて、合計に使用する数字を返します。

つまり、1回目は引数param には {"国語", 75} が渡されます。2回目は{"数学",88}、3回目は{"英語",52}です。このオブジェクトは KeyValuePair(Of String, Integer)で、1つ目の要素(つまり科目)がKeyプロパティ、2つ目の要素(つまり点数)がValueプロパティです。私たちは点数を単純に合計してほしいので、Valueプロパティを返せばよいわけです。

メモ メモ  - Dictionary

Dictionaryの項目の型は常にKeyValuePair型(読み方:キーバリューペア)です。Dictionaryクラスの型パラメーターは項目の型を指定しているのではなく、キーの型と値の型を指定していることになります。

Dictionaryについては初級講座第16回 コレクション1で説明しています。

そこで、合計点を計算するプログラムは次のようになります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim dic As New Dictionary(Of String, Integer) From {{"国語", 75}, {"数学", 88}, {"英語", 52}}
Dim amount = dic.Sum(Function(param As KeyValuePair(Of String, Integer)) As Integer
                         Return param.Value
                     End Function)

このプログラムは期待通り動作して結果は 215 となります。

このラムダ式は値を単純に返しているだけなので、型推論を利用したり、1行形式のラムダ式にすることで次のように簡略に書くこともできます。 

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim dic As New Dictionary(Of String, Integer) From {{"国語", 75}, {"数学", 88}, {"英語", 52}}
Dim amount = dic.Sum(Function(param) param.Value)

いきなり、このプログラムを紹介しても、意味が分からないかもしれない思って今回は順を追って説明しましたが、普通はここまで丁寧な説明はなく、ドキュメント等では前置きなしにこのようなプログラムが出現します。実際、ラムダ式に慣れているプログラマーも瞬時にこのようなプログラムを書きますから、このようなラムダ式に徐々に慣れていくようにしましょう。

 

ラムダ式の良いところは、処理内容を自分で記述できることです。

いろいろ判断したり、変換したり、自由に処理を記述できます。

たとえば、5教科(国語・数学・英語・理科・社会)の点数を基に、3教科(国語・数学・英語)だけの合計を計算するには次のようにします。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim dic As New Dictionary(Of String, Integer) From {{"国語", 75}, {"数学", 88}, {"英語", 52}, {"理科", 98}, {"社会", 60}}
Dim amount = dic.Sum(Function(param) 
                         If param.Key = "国語" OrElse param.Key = "数学" OrElse param.Key = "英語" Then
                             Return param.Value                        
                         Else
                       Return 0
                         End If
                     End Function)

 

1-3.フォルダーサイズの計算

応用例としてフォルダーのサイズを計算するプログラムを紹介します。

フォルダーのサイズは中に入っているファイルサイズの合計です。

次の例は Windowsフォルダー直下のファイルの合計サイズを取得します。

本来、フォルダーのサイズにはその中に含まれるサブフォルダーも含めたすべてのサイズを合計するのですが、ここでは簡略にするために直下のファイルサイズだけを合計します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'Windowsフォルダー(例 C:\windows)
Dim winFolder As New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows))
'中のファイルを取得(サブフォルダー内のファイルは含まれません)
Dim files = winFolder.EnumerateFiles()

'合計サイズ(バイト単位)
Dim totalSize = files.Sum(Function(file) file.Length)

'合計サイズ(MB単位)
Dim totalMBSize = totalSize / 1024 / 1024

Environment.GetFolderPathメソッドはWindowsフォルダーやデスクトップフォルダーなど特殊フォルダーのパスを取得できるメソッドです。どのフォルダーのパスを取得するかは引数に SpecialFolder列挙体を使って指定します。この例では SpecialFolder.Windowsを指定しているので、Windowsフォルダーのパスを取得できます。名前からわかるようにこの例はWindowsでしか実行できません。

EnumerateFilesメソッドは列挙を実行すると対象ファイルを FileInfoクラス で返します。そして、Sumメソッド内のラムダ式で合計すべき値として FileInfoクラスのLengthプロパティを返しています。これはバイト数で表したファイルサイズです。

Windows 10 ではWindowsフォルダー直下にはあまりファイルがないので、合計サイズはMB単位で見れば通常ことたります。

 

操作方法 サブフォルダー内のファイルも対象にするには

winFolder.EnumerateFiles() の部分を winFolder.EnumerateFiles("*", IO.SearchOption.AllDirectories)と修正するとすべてのサブフォルダーも対象となります。

シンボリックリンクなども検索対象に含まれるようになるので、リンクの構成によっては検索が無限ループする可能性があります。

 

2.メソッドチェーン

複数の操作・集計を行うときにはメソッドをつぎつぎとつなげていくメソッドチェーンと呼ばれる記述方法が利用できます。

例として、上記のファイルサイズを計算する例に条件を付けてみましょう。「合計」する処理と「条件」で抽出する処理の2つの操作が発生します。

検索対象に条件を付けるには、ラムダ式内で If 文などを記述するか、Whereメソッドを使用します。

ラムダ式内で If 文を記述する場合は、でてくる集計は結局 Sum だけなのでメソッドチェーンのような書き方はあまりできません。次のようになります。

この例では、1か月以上更新されていないファイルの合計サイズを取得します。最終更新日はFileInfoクラスの LastWriteTimeプロパティで取得できます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'Windowsフォルダー(例 C:\windows)
Dim winFolder As New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows))
'中のファイルを取得(サブフォルダー内のファイルは含まれません)
Dim files = winFolder.EnumerateFiles()

'合計サイズ(バイト単位)
Dim totalSize = files.Sum(Function(file) 
                              If (file.LastWriteTime < Now.AddMonths(-1)) Then
                                  Return file.Length
                              End If
                              Return 0
                          End Function)

'合計サイズ(MB単位)
Dim totalMBSize = totalSize / 1024 / 1024

変数files自体は Windowsフォルダー直下の全ファイルを表しているので、この例のラムダ式は、ファイルの数だけ実行されます。

ラムダ式内で最終更新日を確認し、1か月以上古いものはファイルのサイズを返し、そうでないものは 0 を返すことで、結果として、1か月以上更新されていないファイルの合計サイズを計算することになります。

 

Whereメソッドは第34回でも説明しています。ラムダ式で指定した条件で結果を抽出するメソッドです。

同じことを Where メソッドを使ってやると次のようになります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'Windowsフォルダー(例 C:\windows)
Dim winFolder As New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows))
'中のファイルを取得(サブフォルダー内のファイルは含まれません)
Dim files = winFolder.EnumerateFiles()

'1か月以上更新されていないファイルを取得
Dim oldFiles = files.Where(Function(file) file.LastWriteTime < Now.AddMonths(-1))

'合計サイズ(バイト単位)
Dim totalSize = oldFiles.Sum(Function(file) file.Length)

'合計サイズ(MB単位)
Dim totalMBSize = totalSize / 1024 / 1024

まずWhereメソッドを使って1か月以上更新されていないファイルだけの一覧を変数 oldFiles に格納します。その後、OldFilesからSumメソッドを呼び出して合計を計算します。

このプログラムだと、途中結果を変数に格納しないで、メソッドの戻り値から直接メソッドを呼び出す、「メソッドチェーン」で次のように書くこともできます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'Windowsフォルダー(例 C:\windows)
Dim winFolder As New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows))

'1か月以上更新されていないファイルの合計サイズを取得(サブフォルダー内のファイルは含まれません)
Dim files = winFolder.EnumerateFiles().
    Where(Function(file) file.LastWriteTime < Now.AddMonths(-1)).
    Sum(Function(file) file.Length)

'合計サイズ(MB単位)
Dim totalMBSize = totalSize / 1024 / 1024

このプログラムの方が EnumerateFiles の結果を Where して、その結果を Sum するという流れがわかりやすいので、好まれる傾向にあるようです。

 

3.カウントと判定

3-1.Countメソッドとラムダ式

似たような機能をもう少し取り上げて、この手の処理に慣れていきましょう。

Count メソッドは要素の数を数えます。条件がなければ次のように簡単に使えます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = {"Apple", "Banana", "Cat", "Dog"}

Dim count As Integer = names.Count ' 4 になります。

Countメソッドは、条件に一致するものだけを数えるオーバーロードがあります。条件はもちろんラムダ式で指定します。

たとえば、"A" が含まれている要素の数だけを数えるには次のようにします。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = {"Apple", "Banana", "Cat", "Dog"}

'A または a を含む要素の数を数えます。→ 3 になります。
Dim count As Integer = names.Count(Function(name) name.Contains("a", StringComparison.OrdinalIgnoreCase))

この例では Containsメソッドに省略可能な第2引数で OrdinalIgnoreCase を指定することで大文字と小文字の違いを無視させています。そのため、 Apple の "A" も対象に含まれ、結果は 3 になります。

 

3-2.Countメソッドの実践

もうちょっと違う例でも練習してみましょう。

次の例は Wikipedia のトップページにある画像の URL を一覧で取得します。

.NET Frameworkの場合、この例を実行するには NuGet で System.Net.Http をインストールしてください。→ NuGet の使い方

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim client As New Net.Http.HttpClient
Dim url As String = "https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8"
Dim wikipedia As String = client.GetStringAsync(url).Result

'imgタグを抜き出す
Dim imgs As System.Text.RegularExpressions.MatchCollection =
System.Text.RegularExpressions.Regex.Matches(wikipedia, "\<img.*?src=""(.+?)"".*?/\>")

'URLの一覧をデバッグウィンドウに出力
For Each img As System.Text.RegularExpressions.Match In imgs
    Debug.WriteLine(img.Groups(1).Value)
Next

Debug.WriteLineで出力される場所

正確にはこのプログラムはHTMLの典型的なimgタグからURLを抜き取るだけなので、すべてのサイズ・画像で通用するわけではありません。

URLの抽出には正規表現を使っています。これは私が今回説明したいことではないのですが、少しずついろいろなことに触れることも大切だと思いますので、簡単に何をやっているか説明します。一度読んだくらいでは理解できないと思いますが、そうだとしてもここで立ち止まらないで私が説明したいラムダ式で数を数える説明に進んでいただければと思います。

まず、HttpClientクラスの GetStringAsyncメソッドでWikipediaトップページのHTMLを文字列として取得します。

その後、正規表現を使ってその中から <img src="xxxxxxx" /> のようになっている部分だけをすべて抜き出します。正規表現とは一定の文字列のパターンのことで、パターンで表現された文字列を抽出する非常に優れたツールです。

.NET では System.Text.RegularExpressions名前空間にある Regex クラスを使うことで正規表現を使用できます。

このプログラムで指定しているパターン(正規表現)は "\<img.*?src=""(.+?)"".*?/\>" です。これは <img から始まり、その後、何かが0文字以上続き、 src=" が出現し、その後、何か1文字以上続き " が出現する。その後、何か 0文字以上 続き 最後に > が出現するというパターンです。 .* が何か0文字以上、.+ が何か1文字以上 など記号に意味があります。

URLは src で指定されている文字列の部分で、 ( ) で囲んでいます。( ) で囲まれた部分は、結果を取得するときに Groupsプロパティを使って取得できます。

プログラムの最後では、Groups(1) を使って抜き出した中からsrc属性で指定されているURL部分を表示します。

正規表現内に複数の ( ) を記述すると、Groups(2)、Groups(3)… となります。Groups(0) は imgタグ全体を表します。

操作方法 HTMLの解析

NuGetでインストールできる HtmlAgilityPack を使うと、もう少し直感的にHTMLからさまざまな情報抜き取ることができます。本格的に HTML を操作する機能を作りたい場合は利用を検討してみてください。

 

さて、ではラムダ式を使って、拡張子が jpg であるファイルの数を数えてみましょう。

変数imgsから、さきほどの例のように Count メソッドの引数にラムダ式を指定したいところですが、次のプログラムはエラーで実行できません。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'この例はエラーになります。
Dim jpgCount As Integer = imgs.Count(Function(img)
                                         Dim imageUrl As String = img.Groups(1).Value
                                         Return imageUrl.EndsWith(".jpg")
                                     End Function)

imgs は MatchCollectionクラスであり、このクラスには既に Count というプロパティがあります。そのため、拡張メソッドである Count メソッドは覆い隠され、 「Count」と書くと Count プロパティの方を表す意味になってしまうのです。 

Countメソッドは、IEnumerable(Of T)の拡張メソッドなので、一度 IEnumerable(Of T)型に変換するとCountメソッドの方を呼び出すことができます。

ところで Of T の T とはなんでしょうか? List(Of String) の場合は T は String であるということがわかりやすいのですが、中にはこの MatchCollection のように T がなんだかわかりにくい型もあります。

どうしてもわからない場合は、Microsoft Docsのリファレンスで確認できます。

https://docs.microsoft.com/ja-jp/dotnet/api/system.text.regularexpressions.matchcollection

リファレンスには MatchCollectionクラスが IEnumerable(Of Match)インターフェースを実装していることが明示されているので、IEnumerable(Of T) の T は Match です。名前空間も含めて書くと System.RegularExpressions.Match です。

そのため、次の代入は有効です。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Dim enumImgs As IEnumerable(Of System.Text.RegularExpressions.Match) = imgs

この変数 enumImgs を経由すれば拡張メソッドの方の Count メソッドが呼び出せるので、次のように拡張子が .jpg のファイルを数えることができます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim enumImgs As IEnumerable(Of System.Text.RegularExpressions.Match) = imgs

Dim jpgCount As Integer = enumImgs.Count(Function(img)
                                         Dim imageUrl As String = img.Groups(1).Value
                                         Return imageUrl.EndsWith(".jpg")
                                     End Function)

 

DirectCastを使うと変数で中継しないで、直接 Count メソッドを呼ぶ出すこともできます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim jpgCount As Integer = DirectCast(imgs, IEnumerable(Of System.Text.RegularExpressions.Match)).
                        Count(Function(img)
                                  Dim imageUrl As String = img.Groups(1).Value
                                  Return imageUrl.EndsWith(".jpg")
                              End Function)

 

もう1つ良い方法があります。IEnumerable(Of T)クラスにはAsEnumerable という拡張メソッド(読み方:AsEnumerable=アズエニューメラブル)があり、このメソッドは、対象を IEnumerable(Of T)型に変換してくれます。 T の型が何であるかも任せられるので、気軽に使えます。

これを使うと次のようなプログラムが書けます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim jpgCount As Integer = imgs.AsEnumerable.Count(Function(img)
                                                       Dim imageUrl As String = img.Groups(1).Value
                                                       Return imageUrl.EndsWith(".jpg")
                                                   End Function)

この方法が一番簡潔でよさそうです。プログラムの全体像は次のようになります。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim client As New Net.Http.HttpClient
Dim url As String = "https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8"
Dim wikipedia As String = client.GetStringAsync(url).Result

'imgタグを抜き出す
Dim imgs As System.Text.RegularExpressions.MatchCollection =
System.Text.RegularExpressions.Regex.Matches(wikipedia, "\<img.*?src=""(.+?)"".*?/\>")

'URLの一覧をデバッグウィンドウに出力
For Each img As System.Text.RegularExpressions.Match In imgs
    Debug.WriteLine(img.Groups(1).Value)
Next

'jpgファイルの数を数える
Dim jpgCount As Integer = imgs.AsEnumerable.Count(Function(img)
                                                       Dim imageUrl As String = img.Groups(1).Value
                                                       Return imageUrl.EndsWith(".jpg")
                                                   End Function)

 

メモ メモ  - Countメソッドを呼び出すもう1つ別の方法

拡張メソッドはなんらかのモジュールの共有メソッドですから、モジュール名.拡張メソッド名 の形でも呼び出せます。Countメソッドを実装しているのは Enumerableクラス(モジュール)ですので、次のようになります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim jpgCount As Integer = Enumerable.Count(imgs, Function(img)
                                                       Dim imageUrl As String = img.Groups(1).Value
                                                       Return imageUrl.EndsWith(".jpg")
                                                   End Function)

拡張メソッドについて復習したい人は、初級講座 第32回をご覧ください。

 

メモ メモ  - 私は Text.RegularExpressions ではなく、System.Text.RegularExpressionsと書きたい

ほとんどの場合、Text.RegularExpressions と System.Text.RegularExpressions は同じものを指すので、System. は省略できます。

重要な例外はWindowsフォームアプリケーションの場合です。WindowsフォームアプリケーションではFormクラスを継承したクラスの中に処理を実装していきます。Formクラスには Textというプロパティがあるので、ここで Text.RegularExpressions と書くと、この Text が Text名前空間ではなくTextプロパティであると解釈され、エラーになります。

私は、サンプルプログラムを掲載するときに、「このサンプルはWindowsフォームアプリケーションでは動作しません」とか、「Windowsフォームアプリケーションで実行する場合は、Text. を System.Text. に修正してください」という注意をいちいち書きたくないので、どのような場合でも通用する System.Text.RegularExrpessions という書き方をしているわけです。みなさんは、お好みで System. を省略しても構いません。

なお、「どのような場合でも通用する」は厳密には間違いで、自作のプログラムの中で System という名前空間を定義している場合などは System.Text.RegularExpressions という書き方も通用せず、Global.System.Text.RegularExpressions と書く必要があります。このとてもレアな状況のために、いちいち注意書きや特別な配慮をすることにほとんど意味はないので、私を含め世の中の多くのサンプルではこのケースは考えに入れていません。

 

 

3-3.Contains と Any

Containsは集合の中に条件で指定したものが1つ以上含まれるかを確認します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = {"Apple", "Banana", "Cat", "Dog"}

'Banana が含まれるか確認します。True になります。
Dim result As Boolean = names.Contains("Banana")

 

これと同じ機能の Any (読み方:Any=エニー)というメソッドがあり、Anyの方は、引数をラムダ式で指定できます。

次の例では、"A"または"a" が含まれる項目があるかどうか確認します。StringComparison.OrdinalIgnoreCase が指定されているので、大文字と小文字を区別しません。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = {"Apple", "Banana", "Cat", "Dog"}

'A または a を含む要素があるか確認します。True になります。
Dim result As Boolean = names.Any(Function(name) name.Contains("a", StringComparison.OrdinalIgnoreCase))

 

WhereやCountが使えるようになればAnyのようなラムダ式も同じ要領で使えることがわかると思います。

 

3-4.All

Allメソッド (読み方:All=オール)は、すべての要素が条件を満たすかどうか確認します。

次の例では、すべての要素に"A"または"a" が含まれるか確認します。"Dog" は該当しないため結果は False になります。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = {"Apple", "Banana", "Cat", "Dog"}

'すべての要素が A または a を含むか確認します。False になります。
Dim result As Boolean = names.All(Function(name) name.Contains("a", StringComparison.OrdinalIgnoreCase))

 

4.並び替え

OrderByメソッド(読み方:OrderBy=オーダーバイ)を使うとラムダ式を使った並び替えができます。

引数なしでOrderByを使うと、値の順番に並びます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = {"Cat", "Banana", "Dog", "Apple"}

Dim result = names.OrderBy(Function(name) name)

このプログラムでは result は "Apple", "Banana", "Cat", "Dog" の順に並んだコレクションになります。

この引数のラムダ式は、並び替えに使用する値を返す役割です。文字列そのもので並び変える場合は Function(name) name のような一見無意味なラムダ式を指定することになります。

たとえば、文字数の順に並び変えたい場合は次のようにします。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = {"Cat", "Banana", "Dog", "Apple"}

Dim result = names.OrderBy(Function(name) name.Length)

result は "Cat", "Dog", "Apple", "Banana" となります。

今度は文字列そのものではなく、文字列の長さ、つまり name.Length を基準に並んでほしいのでラムダ式は name.Length を返しています。こうやって見比べると最初の例の Function(name) name の意味もわかりやすいことと思います。

 

次の例は応用です。

Windowsフォルダーにあるファイルを作成日時(CreationTime)が古い順に並べます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'Windowsフォルダー(例 C:\windows)
Dim winFolder As New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows))

'ファイルを作成日時が古い順に並べます。
Dim files = winFolder.EnumerateFiles().OrderBy(Function(file) file.CreationTime)

 

5.IEnumerable(Of T) インターフェース

5-1.コレクションへのラムダ式適用で中心となるインターフェース

ここまで説明してきたSum, Where, Count, Contains, Any, All, OrderBy などの拡張メソッドは、どれも IEnumerable(Of T)インターフェースを拡張します。

このインターフェースは配列やListのようなコレクションをラムダ式で操作するときに中心的な役割を果たす重要な型です。

IEnumerable(Of T)は先頭が I から始まっていることでわかるようにインターフェースです。配列やListクラスもこのインターフェースを実装していますが、これにとどまらず、複数の集合を扱う多くのクラスで実装されています。

このインターフェースが重要なのは、これまで紹介したように多数の拡張メソッドを持ち、コレクション操作の起点となるからです。

IEnumerable(Of T)の 代表的な拡張メソッドは次の通りです。

拡張メソッド 読み方 機能
All オール すべての要素が条件を満たしているか確認します。条件はラムダ式で指定できます。
Any エニー 1つ以上の要素が条件を満たしているか確認します。条件はラムダ式で指定できます。
Append アペンド 後ろに要素を追加します。
Average アベレージ 平均値を求めます。平均の算出ロジックをラムダ式で指定することもできます。
Contains コンテインズ コレクションに特定の要素が含まれているか確認します。
Count カウント 要素の数を取得します。ラムダ式を指定して、条件を満たす要素だけを数えることもできます。
First ファースト 最初の要素を取得します。ラムダ式を指定して、条件を満たす最初の要素を取得することもできます。
Last ラスト 最後の要素を取得します。ラムダ式を指定して、条件を満たす最後の要素を取得することもできます。
Min ミン 最小の要素を取得します。最小の意味はラムダ式で指定できます。
Max マックス 最大の要素を取得します。最大の意味はラムダ式で指定できます。
OfType オブタイプ 要素の中から対象の型の要素だけを抽出します。
OrderBy オーダーバイ 要素を並び変えます。ラムダ式を使って並び替えの基準を指定することもできます。
Prepend プリペンド 先頭に要素を追加します。
Select セレクト 射影を作成します。ラムダ式で変換処理を指定できます。
Skip スキップ 指定された数の要素を飛ばして、残りのすべての要素を取得します。
Sum サム 合計を求めます。合計算出ロジックをラムダ式で指定することもできます。
Take テイク 先頭から指定された数の要素を取得します。
ToArray トゥーアレイ コレクションを配列に変換します。
ToList トゥーリスト コレクションをListに変換します。
Where ホエア ラムダ式で条件を指定して要素を抽出します。

この表を見るとわかるように、多くのメソッドではラムダ式を使って条件や処理内容を指定することができます。ラムダ式とかかわりのないメソッドもあります。

平均値や合計・最大・最小などは、数字をイメージするとラムダ式でやることは特にないように思いますが、コレクションの要素は数字とは限りません。文字列のコレクションもあれば、ファイルのコレクションや、何かのオブジェクトのコレクションもありえます。そのときに平均値や合計・最大・最小がどういう意味なのかをラムダ式で指定することができます。

また、これらのメソッドの多くは戻り値がまた IEnumerable(Of T)なので、続けてまた別の操作を行うということが可能なのも特徴です。メソッドチェーンでプログラムを記述するときにはこの性質が活きます。

また、ToArray, ToList のように、コレクションを配列やListに簡単に変換できる機能もあります。VBの言語機能やフレームワークの機能には、配列やListを必要とするものもあるので、IEnumerable(Of T)から簡単に変換できることで使い勝手が向上します。

 

5-2.プログラム中での型の表現

プログラム中では IEnumerable(Of T)という型の名前を明示的に書かないのが一般的です。明示的に書かなくてもVBが推論してくれます。

次の例では 変数capitals の宣言で型を省略しています。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim content = "Himiko also known as Shingi Wao was a shamaness-queen of Yamatai-koku in Wakoku"
Dim names As New List(Of String)(content.Split(" "))

'先頭が大文字のものだけ抽出するラムダ式を適用(まだ抽出処理は行われない)
Dim capitals = names.Where(Function(word) Char.IsUpper(word(0)))

'capitals の型を確認
Debug.WriteLine(capitals.GetType.FullName)

Debug.WriteLineで出力される場所

最後の行で Debug.WriteLine を使って capitals の型を出力しています。

実行すると出力ウィンドウのデバッグ欄で次のような出力を確認できます。

System.Linq.Enumerable+WhereListIterator`1[[System.String, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]

 

長い出力ですが、クラス名は WhereListIterator であることが読み取れます。

メモ メモ  - 型の名前の読み取り方

System.Linq.Enumerable+WhereListIterator`1[[System.String, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]

後半の [ ] の中はこのクラスがプログラムされているアセンブリ(≒実体はdllファイル)を指しています。たまたま、まったく違うプログラムで同じ名前のクラスが存在することはあり得ますし、元は同じプログラムだけれどもバージョンが違う同じ名前のクラスが存在することもあり得ますから、アセンブリを特定する情報も型の一部というわけです。

System.Linq.Enumerable+WhereListIterator`1 の 部分は、System.Linq名前空間の Enumerableクラスの中に、入れ子になった WhereListIteratorクラスであるということを表しています。

`1 は List(Of String)型のように型パラメーターが1つあることを表しています。

つまり、System.String, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e というアセンブリで定義されている System.Linq 名前空間内の Enumerableクラスの内部クラスである 型パラメーターを1つもつWhereListIterator クラス  と読み解けます。

WhereListIteratorクラスはそもそも外部から呼び出せないようになっており、As WhereListIterator(Of String) と記述してもエラーになります。

それに、上述の表で示したように、WhereやCountなどの便利なたくさんのメソッドはクラスに実装されているのではなく、IEnumerable(Of T)の拡張メソッドとして実装されています。だから、ラムダ式でコレクションを操作するプログラマーのほとんどは型には興味がなくなるのです。

もし、こだわりがあれば As IEnumerable(Of String) などと記述することはありえます。

また、クラスのフィールドやプロパティであったり、メソッドの引数や戻り値の型は、型推論が使用できないので IEnuemrable(Of String) のように型名を明示することになります。

 

6.さまざまな集合操作

6-1.First と FirstOrDefault

IEnumerable(Of T)インターフェースの拡張メソッドは今後もいろいろと使っていくことになると思いますので少し練習しておきましょう。

Firstメソッドは最初の要素を取り出すことができます。

次の例では 変数capitals の宣言で型を省略しています。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim content = "Himiko also known as Shingi Wao was a shamaness-queen of Yamatai-koku in Wakoku"
Dim names As New List(Of String)(content.Split(" "))

'Himiko
Dim word = names.First

次のようにすると、3文字以下の最初の要素を取り出します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim content = "Himiko also known as Shingi Wao was a shamaness-queen of Yamatai-koku in Wakoku"
Dim names As New List(Of String)(content.Split(" "))

'as
Dim word = names.First(Function(name) name.Length <= 3)

該当する要素がないとInvalidOperationException の例外が発生します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim content = "Himiko also known as Shingi Wao was a shamaness-queen of Yamatai-koku in Wakoku"
Dim names As New List(Of String)(content.Split(" "))

'該当する要素がないので例外
Dim word = names.First(Function(name) name.Length > 20)

例外を発生させないで Nothing を返してほしいと思う場合もあるでしょう。

そういう場合はFirstメソッドではなく、FirstOrDefaultメソッド(読み方:FirstOrDefault=ファーストオアデフォルト)を使用します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim content = "Himiko also known as Shingi Wao was a shamaness-queen of Yamatai-koku in Wakoku"
Dim names As New List(Of String)(content.Split(" "))

'該当する要素がないのでNothing
Dim word = names.FirstOrDefault(Function(name) name.Length > 20)

FirstメソッドとFirstOrDefaultメソッドの違いは、ここで説明した該当する要素がない場合の違いだけです。

要素が数字の場合、FirstOrDefaultメソッドは該当するものがないと 0 を返します。

 

6-2.Last と LastOrDefault

LastメソッドとLastOrDefaultメソッド(読み方:LastOrDefault=ラストオアデフォルト)は、それぞれFirstメソッドとFirstOrDefaultメソッドと同じですが、最初ではなく最後の要素を取得する点に違いがあります。

次の例は文字列の最後の文字の「校」を取得します。String型もIEnumerable(Of Char)を実装しているのでこれらの拡張メソッドを使用できます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim content = "Visual Basic 中学校"
'校
Dim letter As Char = content.Last

 

6-3.Append と Prepend

Appendメソッドは最後に要素を追加し、Prependメソッドは先頭に要素を追加します。

これらは元となる集合を変更するのではなく、要素を追加した新しい集合を返します。

Appendを使うと、要素を追加するのが面倒な配列にも簡単に要素を追加できます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names As String() = {"Apple", "Banana", "Cat", "Dog"}

Dim result = names.Append("Eagle")

List や Dictionaryなどのコレクションは Addメソッドを使って末尾に要素を追加することは簡単なのですが、Prependを使うと先頭に要素を追加するのも楽です。

 

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names As New List(Of String) From {"Apple", "Banana", "Cat", "Dog"}

Dim result = names.Prepend("Let's sing")

 

6-4.Min と Max

Min と Max は集合の中から最小値と最大値を抜き出します。

次の例では 最小値である 2 を取得します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim numbers = {5, 8, 2, 5, 23, 6}

Dim result = numbers.Min

 

値を直接比較して最小・最大を決めるのではなく、ラムダ式を使って変換した値を基に最小・最大を判断することもできます。

次の例は、偶数の中で最大のものを取得します。結果、8 を取得します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim numbers = {5, 8, 2, 5, 23, 6}

Dim result = numbers.Max(Function(number)
                             If number Mod 2 = 0 Then
                                 Return number
                             Else
                                 Return Integer.MinValue
                             End If
                         End Function)

Maxはラムダ式の戻り値を基に判断します。ラムダ式の中では偶数なら値をそのまま返し、奇数なら Integer.MinValue、つまり、-2147483648 というとても小さな値を返します。

このラムダ式を通すと値は次のようになります。

基の値 ラムダ式が返す値
5 -2147483648
8 8
2 2
5 -2147483648
23 -2147483648
6 6

というわけで、最大値は 8 を返した 8 になるわけです。

 

6-5.OfType

OfTypeは値ではなく型に着目します。集合の中で指定した型に合致する要素だけを抽出します。

いろいろな型が混在する場合にとても便利です。

次の例ではいろいろな型からなる集合 mess に対して OfTypeメソッドを使って、Integerのものだけの集合、Stringのものだけの集合など、型ごとに集合を抽出します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim mess As New List(Of Object) From {2, "あ", 3, "い", New Date(2022, 6, 27), 6.5}

Dim ints = mess.OfType(Of Integer) '2, 3
Dim strings = mess.OfType(Of String) '"あ", "い"
Dim dates = mess.OfType(Of Date) '2022/6/27
Dim doubles = mess.OfType(Of Double) '6.5

 

WindowsフォームアプリのようなGUIアプリケーションでは、ボタンやテキストボックスなどの視覚的なコントロールを集合として操作できるので、OfTypeメソッドを使うと、すべてのTextBoxに対して同じ処理を実行するということが簡単にできて重宝します。

次の例は Windowsフォームアプリでフォームに直接貼り付いているすべてのTextBoxのテキストを Hello! にします。Panel や GroupBox などの内部にある TextBox は対象外です。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Me.Controls.OfType(Of TextBox).ToList.ForEach(Sub(textBox) textBox.Text = "Hello!")

End Sub

フォームに直接貼り付いているコントロールの集合は Controls プロパティで取得できます。そこから、OfTypeメソッドを使ってTextBoxだけの集合を作り、ToList で List(Of TextBox)型に変換します。この型にはすべての要素にたいしそれぞれラムダ式の処理を実行してくれる便利な ForEachメソッドがあるので、これを使ってTextプロパティを書き換えています。

 

6-6.Skip と Take

Skip と Take は位置に注目します。

Skip は 集合の中で先頭のいくつかの要素を除いた残りの要素の集合を取得します。

次の例では先頭の2要素が取り除かれます。結果としてresultは "Cat", "Dog", "Eagle", "Fox" となります。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = {"Apple", "Banana", "Cat", "Dog","Eagle", "Fox"}

Dim result = names.Skip(2)

Takeは逆に先頭から指定して数の要素だけを集めた集合を作ります。

次の例では先頭の2要素だけが採用されます。結果としてresultは"Apple", "Banana"となります。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = {"Apple", "Banana", "Cat", "Dog","Eagle", "Fox"}

Dim result = names.Take(2)

 

6-7.ToArray と ToList

ToArray と ToList は要素は何も変更せず集合の型を配列またはList(Of T)に変換します。

IEnumerable(Of T)は集合の操作には便利なのですが、フレームワークの機能の中には配列やListを前提としているものもいろいろあります。WhereやOrderBy、Skip などで思う存分集合を操作したら、最後に ToArray や ToList などを使って見慣れた配列やList(Of T)に型を変換して、他の機能に渡すことができます。

私はList(Of T)のForEachメソッドが大好きなのでこれを使いたいためだけに ToList することもあります。