LoginSignup
9
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-10-13

はじめに

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

まずは普通の関数として書いてみる

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

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で置き換えると、こんな感じになります。1行目でLinqをImportsするのを忘れないようにしてください。

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

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の部分をひとつひとつ見て行きましょう。

LINQ
source.ToCharArray().

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

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

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メソッドを呼び出しています。

LINQ-Where
Where(Function(ch) Char.IsDigit(ch)).

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

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

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

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

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

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

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

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

Aggregate

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

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

Aggregateメソッドに与えている単一行の無名関数(ラムダ式)
Function(result As String, ch As Char) result + ch

Aggregateメソッドは、まず resultに 初期値 String.Emptyを設定してから、この無名関数に result と 配列の第一要素を入れて呼び出します。無名関数を仮にagg、Aggregateメソッドに渡される配列データをarrとして、処理のイメージを見てみましょう。

Aggreageteの処理イメージ
Dim result = String.Empty
dim ch = arr(0)
result = agg(result, ch)

この結果、resultには配列の第一要素(0番目の要素)が文字列として入っています。Aggregateメソッドは、そのまま次の要素(1番目の要素)を取得し、続けてaggを呼び出してresultに格納します。

Aggreageteの処理イメージ(続き)
ch = arr(1)
result = agg(result, ch)

そして、2番目の要素、3番目の要素…と、次々と呼び出してはresultに格納していきます。

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になるわけです。

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

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

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

こんな感じで使えます。

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

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

それでは、答えです。

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

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6