[VB.Net]LINQを使って文字列とかをいじってみる

#はじめに
**なんかLINQって難しそうだなぁ**と思っていませんか？ VB.Netユーザーの皆さん、もっと**気軽に**使ってみましょう！
この記事では、文字列をToCharArray()で文字型配列に分解してLINQで処理することで、「**あ、LINQって意外と簡単じゃん！**」て思って貰う事を目標とします。あとで、数値配列をいじったり、Aggregateで集計したりもします。

#まずは普通の関数として書いてみる
まずは、LINQを使わない、**普通の関数**から始めてみましょう。

```VB:Form1.vb
    '文字列から数字だけを抜き出す関数
    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で置き換えると、こんな感じになります。１行目でLinqをImportsするのを忘れないようにしてください。

```VB:Form1.vb
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メソッドをチェーンのように繋げています。こういうものをメソッドチェーンと呼びます。
下のように、ドット部分でメソッドチェーンに改行を入れると少し見やすくなるかもしれません。

```VB:Form1.vb
    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の部分**をひとつひとつ見て行きましょう。

```VB:LINQ
source.ToCharArray().
```

この部分はLINQではないのでわかると思います。文字列を、文字型の配列 Char() に変換しています。**LINQは配列やList、HashTable、Dictionary等の、コレクション型を処理する為のライブラリ**ですので、文字列型を一旦、1文字単位で処理できる「**文字列型の配列**」に変換しているわけです。

戻り値はこんな感じになります。

```VB:ToCharArray()の戻り値
{"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メソッドを呼び出しています。

```VB:LINQ-Where
Where(Function(ch) Char.IsDigit(ch)).
```
Char()型の配列に対して**Whereメソッド**を呼んでいます。Whereでは、コレクションや配列を、**指定した条件で絞り込む**事ができます。引数として与えるのは、「**条件判定関数**」です。配列の各要素、ここでは1文字分のデータを引数として受け取って、それが条件に合う文字ならば **True**、合わない文字ならば **False**を返す、という関数が、今回の条件判定関数になります。

具体的には、以下の部分が条件判定関数です。

```VB:Whereメソッドに与えている単一行の無名関数（ラムダ式）
Function(ch) Char.IsDigit(ch)
```

chはChar型の引数です。**今回処理するのがChar型の配列なので、条件判定関数の引数もChar型**になります。そのchを、Char.IsDigit(Char) を使って「数字かどうか」を判定しています。戻り値はそのままTrue/Falseになります。単一行なのでEnd Functionがありませんが、複数行にしてももちろん大丈夫です。

```VB:Whereメソッドに与えている無名関数（複数行の場合）
Function(ch) 
    Dim result As Boolean = Char.IsDigit(ch)
    Return result
End Function    
```

複数行の場合はReturnが必要になるのでご注意下さい。
**Whereメソッドは、配列の各要素（1文字ずつのデータ）に対してこの無名関数を呼び出し、結果がTrueのものだけを残した配列を新たに作ります**。

```VB:Whereメソッドの戻り値
{ "0"c, "1"c, "2"c, "0"c, "4"c, "4"c, "4"c, "4"c, "4"c, "4c"}
```

数字文字以外は綺麗に除去されています。**Whereメソッドらくちん！**

#Aggregate
次は、これを結果文字列に連結していく部分です。これはAggregateメソッドを使います。
他の関数型言語を学んだ事がある方ならば、これは「**fold（畳み込み関数）**」だと言えば、分かりやすいでしょうか。
ちょっと複雑なのでゆっくり見て行きましょう。

```VB:LINQ-Aggregate
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を連結して新しい文字列として返す**、というものです。

```VB:Aggregateメソッドに与えている単一行の無名関数（ラムダ式）
Function(result As String, ch As Char) result + ch
```
Aggregateメソッドは、まず resultに 初期値 String.Emptyを設定してから、この無名関数に result と 配列の第一要素を入れて呼び出します。無名関数を仮に**agg**、Aggregateメソッドに渡される配列データを**arr**として、**処理のイメージ**を見てみましょう。

```VB:Aggreageteの処理イメージ
Dim result = String.Empty
dim ch = arr(0)
result = agg(result, ch)
```
この結果、resultには配列の第一要素（0番目の要素）が文字列として入っています。Aggregateメソッドは、そのまま次の要素（1番目の要素）を取得し、続けてaggを呼び出してresultに格納します。

```VB:Aggreageteの処理イメージ（続き）
ch = arr(1)
result = agg(result, ch)
```
そして、2番目の要素、3番目の要素…と、次々と呼び出してはresultに格納していきます。

```VB:Aggreageteの処理イメージ（続き2）
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になるわけです。

```VB: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型の配列を使った、簡単な例を少し見てみましょう。

```VB:偶数だけを抜き出すLINQ
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の為の記法で書き表すと、次のような感じになります。

```VB:クエリ式で書いた場合
'奇数だけ抜き出す
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()** 等です。

こんな感じで使えます。

```VB:LINQ-Aggregate
    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を使ってシンプルに書いてみましょう。クエリ式じゃなくて普通のメソッド呼び出し形式で良いですよ。

…どうですか？ イメージできましたか？

それでは、答えです。

```VB:Form1.vb
    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ユーザーの皆様が、過去の呪縛から解き放たれる日が来ることを願っています。