はじめに
なんかLINQって難しそうだなぁと思っていませんか? VB.Netユーザーの皆さん、もっと気軽に使ってみましょう!
この記事では、文字列をToCharArray()で文字型配列に分解してLINQで処理することで、「あ、LINQって意外と簡単じゃん!」て思って貰う事を目標とします。あとで、数値配列をいじったり、Aggregateで集計したりもします。
まずは普通の関数として書いてみる
まずは、LINQを使わない、普通の関数から始めてみましょう。
'文字列から数字だけを抜き出す関数
Private Function RemoveNonNumericChars(source As String) As String
'空っぽの文字列から始めて
Dim result As String = String.Empty
'引数の文字列を1文字ずつチェックしながら
For Each ch As Char In source.ToCharArray()
If Char.IsDigit(ch) Then
'数字だったら繋ぎ合わせていく。
result = result + ch
End If
Next
'最後に繋いだ結果を返す
Return result
End Function
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Dim str = "tel:0120-444-444"
Dim numericStr = RemoveNonNumericChars(str)
Debug.Print("numericStr = " + numericStr)
End Sub
どういう処理かというと、文字列を与えると、その文字列を「Char型の配列」に変換(ToCharArray()で変換できます)して、1文字ずつFor Eachでループを回しながら、**Char.IsDigit(ch)**を使って「それが数字の文字かどうか」を判定し、数字文字ならば結果文字列に繋いでいく、というものです。"a1b2c3"ならば、'a'や'b','c'が弾かれて、"123"が返ってくる感じです。
LINQで置き換えてみる
これをLINQで置き換えると、こんな感じになります。1行目でLinqをImportsするのを忘れないようにしてください。
Imports System.Linq
Public Class Form1
Private Sub Button2_Click(sender As System.Object, e As System.EventArgs) Handles Button2.Click
Dim str = "tel:0120-444-444"
'LINQで同じ処理を書いた
Dim numericStr = source.ToCharArray().Where(Function(ch) Char.IsDigit(ch)).Aggregate(String.Empty, Function(result As String, ch As Char) result + ch)
Debug.Print("numericStr = " + numericStr)
End Sub
End Class
WhereメソッドとAggregateメソッドをチェーンのように繋げています。こういうものをメソッドチェーンと呼びます。
下のように、ドット部分でメソッドチェーンに改行を入れると少し見やすくなるかもしれません。
Private Sub Button2_Click(sender As System.Object, e As System.EventArgs) Handles Button2.Click
Dim str = "tel:0120-444-444"
'LINQで同じ処理を書いた
Dim numericStr = source.ToCharArray().
Where(Function(ch) Char.IsDigit(ch)).
Aggregate(String.Empty, Function(result As String, ch As Char) result + ch)
Debug.Print("numericStr = " + numericStr)
End Sub
LINQの部分をひとつひとつ見て行きましょう。
source.ToCharArray().
この部分はLINQではないのでわかると思います。文字列を、文字型の配列 Char() に変換しています。LINQは配列やList、HashTable、Dictionary等の、コレクション型を処理する為のライブラリですので、文字列型を一旦、1文字単位で処理できる「文字列型の配列」に変換しているわけです。
戻り値はこんな感じになります。
{"t"c, "e"c, "l"c, ":"c, "0"c, "1"c, "2"c, "0"c, "-"c, "4"c, "4"c, "4"c, "-"c, "4"c, "4"c, "4c"}
Where
この戻り値に対して、次のWhereメソッドを呼び出しています。
Where(Function(ch) Char.IsDigit(ch)).
Char()型の配列に対してWhereメソッドを呼んでいます。Whereでは、コレクションや配列を、指定した条件で絞り込む事ができます。引数として与えるのは、「条件判定関数」です。配列の各要素、ここでは1文字分のデータを引数として受け取って、それが条件に合う文字ならば True、合わない文字ならば Falseを返す、という関数が、今回の条件判定関数になります。
具体的には、以下の部分が条件判定関数です。
Function(ch) Char.IsDigit(ch)
chはChar型の引数です。今回処理するのがChar型の配列なので、条件判定関数の引数もChar型になります。そのchを、Char.IsDigit(Char) を使って「数字かどうか」を判定しています。戻り値はそのままTrue/Falseになります。単一行なのでEnd Functionがありませんが、複数行にしてももちろん大丈夫です。
Function(ch)
Dim result As Boolean = Char.IsDigit(ch)
Return result
End Function
複数行の場合はReturnが必要になるのでご注意下さい。
Whereメソッドは、配列の各要素(1文字ずつのデータ)に対してこの無名関数を呼び出し、結果がTrueのものだけを残した配列を新たに作ります。
{ "0"c, "1"c, "2"c, "0"c, "4"c, "4"c, "4"c, "4"c, "4"c, "4c"}
数字文字以外は綺麗に除去されています。Whereメソッドらくちん!
Aggregate
次は、これを結果文字列に連結していく部分です。これはAggregateメソッドを使います。
他の関数型言語を学んだ事がある方ならば、これは「fold(畳み込み関数)」だと言えば、分かりやすいでしょうか。
ちょっと複雑なのでゆっくり見て行きましょう。
Aggregate(String.Empty, Function(result As String, ch As Char) result + ch)
Aggregateメソッドは、配列/コレクションのデータを、単一のデータに「集計」します。その為に、まず resultに初期値(ここでは String.Empty を初期値に指定しています)を設定し、配列の最初の要素から順番に1つずつ、resultに対して「適用」していきます。ここではその「適用」処理が、Aggregateメソッドの第二引数として与えている無名関数になります。なお、Aggregateメソッドの第一引数 String.Empty は、result の初期値です。
無名関数だけを抜き出してみましょう。内容は、文字列型のresultに、引数のchを連結して新しい文字列として返す、というものです。
Function(result As String, ch As Char) result + ch
Aggregateメソッドは、まず resultに 初期値 String.Emptyを設定してから、この無名関数に result と 配列の第一要素を入れて呼び出します。無名関数を仮にagg、Aggregateメソッドに渡される配列データをarrとして、処理のイメージを見てみましょう。
Dim result = String.Empty
dim ch = arr(0)
result = agg(result, ch)
この結果、resultには配列の第一要素(0番目の要素)が文字列として入っています。Aggregateメソッドは、そのまま次の要素(1番目の要素)を取得し、続けてaggを呼び出してresultに格納します。
ch = arr(1)
result = agg(result, ch)
そして、2番目の要素、3番目の要素…と、次々と呼び出してはresultに格納していきます。
ch = arr(2)
result = agg(result, ch)
ch = arr(3)
result = agg(result, ch)
ch = arr(4)
result = agg(result, ch)
結果的にresultには、全ての文字型配列の要素が、1つの文字列として連結されたものが格納されています。
これが、Aggregateメソッドが内部で行っていることです。プログラマーが与えなければならないのは、resultの初期値と、そのresultに対して各要素をどのように「適用」していくのか、という処理を定義した無名関数のみです。
Aggregateの戻り値は、指定した無名関数の戻り値の型と同じになります。今回は文字列を返しているので文字列型となっていますが、別に文字列型を返さずとも、各CharのASCII値を合計したInteger型でも構わないのです。
このように、配列やコレクションの各要素を順番に同じ方法で処理していき、単一の値を作り出すことを「畳み込み」と呼びます。他の関数型言語ではfoldという名前の関数名になっている事が多いようなので、覚えておくと良いかと思います。
これらを全て繋げると、冒頭のLINQになるわけです。
Dim numericStr = source.ToCharArray().Where(Function(ch) Char.IsDigit(ch)).Aggregate(String.Empty, Function(result As String, ch As Char) result + ch)
数値配列をWhereで絞り込む
Aggregateは慣れるまでちょっと考え方が難しいのですが、配列データをWhereメソッドで絞り込む、というだけなら、充分使えそうな気がしませんか?
Integer型の配列を使った、簡単な例を少し見てみましょう。
Dim arr() As Integer = {1, 2, 3, 4, 5, 6, 7, 8, 9}
'偶数だけを抜き出す
Dim even() As Integer = arr.Where(Function(item) item Mod 2 = 0).ToArray()
For Each i In even
Debug.Print(i.ToString()) '2 4 6 8 が出力される
Next
Whereメソッドに与えている無名関数の意味は、分かりますよね? i Mod 2 = 0 は、i が偶数の場合のみ Trueが返る式なので、結果的に偶数だけが抜き出される、という訳です。
今回はAggregateで単一データにしていないので、最後はToArray()で配列として取得する必要があります。
クエリ式
ちなみに、これを「クエリ式」と呼ばれる、SQLのようなLINQの為の記法で書き表すと、次のような感じになります。
'奇数だけ抜き出す
Dim odd = From item In arr Where item Mod 2 = 1
For Each i In odd
Debug.Print(i.ToString())
Next
今度は奇数を抜き出す処理にしてみました。「Function」とかの余計な装飾を全部省略できるので楽ちんですね!
尚、クエリ式の戻り値はIEnumerable型で、上の例でいうとoddは配列ではありません。まだクエリ式自体、実行されておらず、「関数」の形のまま保持されている状態です。For Each等で1つずつ値を取り出すと、実際に実行されて結果が取得できます。もしまとめて配列で取得したい場合は、odd.ToArray() で取得可能です。
Aggregateをクエリ式で使う
ついでですので、Aggregateをクエリ式で使う場合のお話も少し。クエリ式でAggregateを使う場合は、基本的には、既に用意されている集計用の関数を用いる使い方になります。用意されているのは、数値型を合計する Sum() や、数値型の最大数・最小数を求める Max()、Min()、平均を求める Average() 等です。
こんな感じで使えます。
Private Sub Button4_Click(sender As System.Object, e As System.EventArgs) Handles Button4.Click
Dim arr() As Integer = {1, 2, 3, 4, 5, 6, 7, 8, 9}
'合計
Dim iSum As Integer = Aggregate item In arr Where item > 7 Into Sum()
Debug.Print("Sum = " + iSum.ToString()) ' Sum = 17
'最大
Dim iMax As Integer = Aggregate item In arr Where item < 4 Into Max()
Debug.Print("Max = " + iMax.ToString()) ' Max = 3
'最少
Dim iMin As Integer = Aggregate item In arr Where item < 4 Into Min()
Debug.Print("Min = " + iMin.ToString()) ' Min = 1
'平均
Dim dblAverage As Double = Aggregate item In arr Where item > 5 Into Average()
Debug.Print("Average = " + dblAverage.ToString()) ' Average = 3
End Sub
ちょっとした合計を求めたい時などには便利かもしれませんね…と言いたいところなのですが、実は配列自体にSum()やMax()、Average()などのメソッドがありますので、そんなに出番はないかもしれないですね(汗)
でも、仕組みとして知っておくと、いろいろと応用可能だと思います。
クイズ
最後に、この記事の総括として、「文字列の配列から、長さが5文字以下のものを削除する」という処理を、LINQを使ってシンプルに書いてみましょう。クエリ式じゃなくて普通のメソッド呼び出し形式で良いですよ。
…どうですか? イメージできましたか?
それでは、答えです。
Private Sub Button6_Click(sender As System.Object, e As System.EventArgs) Handles Button6.Click
Dim strarr() As String = {"apple", "banana", "", "pine", "", "orange"}
Dim result() = strarr.Where(Function(s) s.Length > 5).ToArray()
result.ToList().ForEach(Sub(s) Debug.Print(s)) 'banana orange が出力される
End Sub
イメージと合っていたでしょうか?
最後の1行は、おまけです。ForEachという、戻り値の無い無名Subプロシージャを引数に取るメソッドで、配列やコレクションの各要素についてそのSubプロシージャを実行してくれます。ForEachは、LINQのメソッドチェーンの最後につけて結果を出力したりできて便利なので、覚えておくと良いでしょう。
あとがき
先日、VB.Netの仕事でLINQを使ったら、「LINQは使っていないので使わないでください」と言われました。こういう記事を書く事で、VB.Netユーザーの皆様が、過去の呪縛から解き放たれる日が来ることを願っています。