更新日: 2010 年 5 月 7 日

執筆者: エディフィストラーニング株式会社 矢嶋 聡

この記事は、「MSDN プログラミング シリーズ」として発行している技術書籍「ステップアップ Visual Basic 2010 ~開発者がもう一歩上達するための必読アドバイス」(日経 BP 社刊) を基に先進的なテクニックを紹介しています。


目次

  1. はじめに
  2. ラムダ式が必要となる背景 ~式を渡すには~
  3. デリゲートを使用して式を渡す
  4. ラムダ式を使用して式を渡す ~ラムダ式の基本~
  5. ラムダ式の実践的な応用例

1. はじめに

Visual Basic では、Visual Basic 2008 がリリースされた際に「ラムダ式」が導入され、さらにVisual Basic 2010 には「ラムダ式」の新しい構文が追加されました。この「ラムダ式」は、関数などのプロシージャを簡潔に表現する 1 つの表記方法です。

この表記方法は、厳密にいうと .NET Framework の実行環境が持つ仕組みではなく、Visual Basic のコンパイラによって処理される Visual Basic の言語仕様上の構文です。(Visual C# や Visual C++ にも同様の構文があります。) LINQ 関連のクラス ライブラリでも「ラムダ式」の使用を想定したメソッドなども定義されており、LINQ でより高度な表現を行う際に、「ラムダ式」を使用する状況に実際に出くわすはずです。

そこで、今回から 2 回に分けて「ラムダ式」について説明していきます。まず今回は、ラムダ式が必要な背景やその基本的な使い方などについて取り上げます。

ページのトップへ


2. ラムダ式が必要となる背景 ~式を渡すには~

まず、ラムダ式を使用しないサンプル コードを使用して、ラムダ式が必要となる背景を考えてみましょう。次のサンプル コードは、Integer 型の要素のコレクションから、「特定の数値よりも小さい」という条件を満たす要素を抽出して、それを新たなコレクションとして作成するものです。(このサンプル コードを実行するためには、Windows フォーム アプリケーション プロジェクトを作成し、ボタン 1 つ、リストボックス 1 つをフォームに貼り付け、下記の通りボタンのイベント ハンドラーを生成します。また、モジュール MyUtil.vb を追加し、下記のサンプルと同じになるように作成してください。)

例 1. Integer 要素のコレクションから条件を満たす要素のコレクションを作る

Public Class Form1

Private Sub Button1_Click(...) Handles Button1.Click ←Clickイベントハンドラ
Dim data() As Integer = {10, 20, 30, 40, 50, 60, 70}
Dim results As IEnumerable(Of Integer)
results = MyUtil.FilterLowerBy(data, 40) ←[1]
For Each dt In results
ListBox1.Items.Add(dt)
Next
End Sub

End Class

Public Module MyUtil

Public Function FilterLowerBy(ByVal source As IEnumerable(Of Integer),     ←[2]
ByVal condition As Integer                      ←[3]
) As IEnumerable(Of Integer)
Dim list As New List(Of Integer)
For Each dt As Integer In source
If dt < condition Then              ←[4]
list.Add(dt)                     ←[5]
End If
Next
Return list
End Function

End Module

この例では、実際に抽出を行うのは [2] の FilterLowerBy メソッドです。このメソッドで扱う Integer 型要素のコレクションは、For Each ループで使用できるものにするため、IEnumerable (Of Integer) 型にしました。抽出対象のコレクションは第 1 引数に渡り、抽出結果のコレクションを戻り値で返します。(IEnumerable (Of Integer) については、第 19 回「ジェネリック インタフェースとジェネリック メソッド」を参照してください。)

特に抽出に係るのは、[3] と [4] の太字部分です。[3] で抽出の基準となる Integer 型の値を仮引数 condition で受け取り、For Each ループ ブロックの中の [4] で、この値より小さい要素を求めるために、比較評価を行っています。条件を満たすものは、[5] で list コレクションに追加しています。

さて、この例では、40 未満を抽出しましたが、40 を超えるものや、20 以上かつ 50 以下というように、異なる様々な条件評価をしたい場合、どうすればよいでしょうか。もっとも単純な方法は、条件評価式の形式ごとに、FilterBiggerBy メソッド、FilterBetween メソッドというように、メソッドを実装することです。しかし、これではメソッドの定義が非常に多くなり、きりがありません。

かといって、第 2 引数に抽出用の条件評価式を直接渡すというのも、単純には書けません。例えば、次のように書いても意味がありません。

例 2. 評価式「20 以上かつ 50 以下」を渡す?

Visual Basic
results = MyUtil.FilterLowerBy(data, x >= 20 AndAlso y <= 50)  '意味がない!
 

そもそも、変数 x が未定義という問題もありますが、仮に書けたとしても、メソッドを呼び出す際に記述した式は、式そのものが呼び出し先に渡るのではなく、呼び出す前に式の結果を算出して、算出結果が渡るだけです。つまり、この場合は単に True か False という定数値が FilterLowBy メソッドに渡るだけであり、呼び出されたメソッドの中では、評価する式として使用できません。

ページのトップへ


3. デリゲートを使用して式を渡す

このような式を渡す方法の 1 つは、前回取り上げたデリゲートを使用する方法があります。

以下の例は、呼び出す側が任意の評価式を指定できるようにデリゲートを使用するよう変更した例です。

例 3. デリゲートを使用して評価を行う関数を渡す

Public Class Form1

Private Sub Button1_Click(...) Handles Button1.Click ←Clickイベントハンドラ
Dim data() As Integer = {10, 20, 30, 40, 50, 60, 70}
Dim results As IEnumerable(Of Integer)
results = MyUtil.FilterBy(data, New Func1(AddressOf Me.MyFilter))       ←[1]
For Each dt In results
ListBox1.Items.Add(dt)
Next
End Sub

Private Function MyFilter(ByVal dt As Integer) As Boolean                   ←[2]
Return dt < 40
End Function

End Class

Public Delegate Function Func1(ByVal v As Integer) As Boolean         ←[3]

Public Module MyUtil

Function FilterBy(ByVal source As IEnumerable(Of Integer),          ←[4]
ByVal condition As Func1) As IEnumerable(Of Integer) ←[5]
Dim list As New List(Of Integer)
For Each dt As Integer In source
If condition(dt) Then                                        ←[6]
list.Add(dt)
End If
Next
Return list
End Function

End Sub

この例の [4] にある FilterBy メソッドでは、前述の FilterLowerBy メソッドとは異なり、 [5] や [6] の太字部分のように変更しました。[5] では、仮引数 conditionにFunc1 型のデリゲート インスタンスを受取ります。このデリゲートは、[3] のデリゲート宣言にあるように、Integer 型の引数を受取り、Boolean 型の戻り値を返す関数 (Function プロシージャ) を呼び出すためのものです。そして [6] では、このデリゲート インスタンス (引数 condition) を呼び出して評価しています。

このようにすれば、抽出処理の呼び出し元である Form1 クラス側で、[2] のメソッドの中に、任意の評価条件を書くことがてきます。任意の条件式を指定して、抽出できるようになりました。ただし、問題が解決したものの、1 つ難点を挙げるとすれば、たった 1 行の式 (ここでは、"dt < 40") のためにメソッド MyFilter を定義しないといけない点です。

そもそも、メソッドは "ある程度まとまった処理" (意味的に分割すべき 1 つの処理) を 1 つのメソッドとして記述し、再利用することで、工数を節約するという性格があります。しかし、上記では、ただ "処理の自由度を持たせたい" というシステム的な理由だけで分割をおこなっており、例えて言うならば、100 行のコードがあるとき、1 行ごとに合計 100 個のメソッドに分割するようなものです。メソッドとして定義するには粒度がやや低い感があります。この 1 行の式を引数として渡す簡潔な表現方法はないでしょうか。

そこで、ラムダ式の登場です。

ページのトップへ


4. ラムダ式を使用して式を渡す ~ラムダ式の基本~

例 3 の [1] と [2] のように、メソッドを定義して、そのメソッドのデリゲート インスタンスを渡したい状況では、ラムダ式を用いると簡潔に表現できます。次の例は、ラムダ式 (太字部分) を使用するように、例 3 の Form1 クラスを書き換えたものです。

例 4. ラムダ式の利用

Public Class Form1

Private Sub Button1_Click(...) Handles Button1.Click ←Clickイベントハンドラ
Dim data() As Integer = {10, 20, 30, 40, 50, 60, 70}
Dim results As IEnumerable(Of Integer)
results = MyUtil.FilterBy(data, Function(x) x < 40)             ←[1]
For Each dt In results
ListBox1.Items.Add(dt)
Next
End Sub

End Class

FilterBy メソッドのほうは、変更ありません。この例 4 の Form1 クラスでは、MyFilter メソッドの定義は必要ありません。[1] の太字部分のように条件式を直接記述できます。

これは、例 3 の [1] および [2] と実質的に等価です。例 4 の [1] のラムダ式は、関数を定義するのと同時に、そのデリゲート インスタンスを作成する作用があり、そのデリゲート インスタンスが FilterBy メソッドの引数として渡ります。

構文の書き方を詳しくみてみましょう。上記の構文は、引数の型の定義が省略されており、その省略部分も書くと次のようになります。

例 5. 引数の型を定義したラムダ式

results = MyUtil.FilterBy(data, Function(x As Integer) x < 40)

ラムダ式では、関数を定義する際に Function キーワードに続けて、関数名 (プロシージャ名) を書かずに、続けて引数リストを定義します。一般に、このような関数は、名前が無いメソッドなので「匿名メソッド」と呼ばれることもあります。

引数リストの書き方は、通常のメソッドとほぼ同様で (一部制約があります)、ここでは Integer 型の任意名 x の仮引数が定義されています。そして、その後には関数の本体として、1 行だけの構文 (この構文は値を返す必要があります) を記述します。ここでは、仮引数 x の値が 40 未満であるか評価しています。実質的には「Return x < 40」の意味となり、暗黙的に評価結果を返します。逆に、Return を明記できません。また、この形式では本体は 1 行であり、続けて End Function を書くことはできません。

戻り値の型については、As 句を使って明示することはできません。戻り値は、コンパイラによって推論されます。例 4 の [1] では、FilterBy メソッドの引数として渡しているので、例 3 の Func1 型に合致するように解釈され、戻り値は Boolean 型とみなされます。同様に引数も推論できるので、結局のところ、例 4 のようにラムダ式の引数リストでは "As Integer" も省略できます。

Note: Visual Basic 2008 のラムダ式では、上記のように Function キーワードで始まる、関数本体が 1 行だけのものが記述可能でした。しかし Visual Basic 2010 では、新機能として、Sub キーワードで始まるラムダ式や、関数本体が複数行のものも可能になりました。これらのバリエーションについては次回に取り上げます。

このように引数や戻り値など決められたルールさえ守れば、任意の条件式を記述できます。例えば、20 以上かつ 50 以下という複合条件の場合も、次のように簡潔に表現できます。

例 6. 20 以上かつ 50 以下をラムダ式で表わす

results = MyUtil.FilterBy(data, Function(x) x >= 20 AndAlso x <= 50)

最後に、より身近で実践的なラムダ式の応用例を見ておきましょう。

ページのトップへ


5. ラムダ式の実践的な応用例

既に触れたように、ラムダ式は実質的にはデリゲート インスタンスなので、ラムダ式を受け取る仮引数はデリゲート型 (例えば、例 3 の [5] など) である必要があります。

Note: 実はラムダ式には、デリゲートの代替となる簡潔表現としての役割のほかに、「式ツリー」と呼ばれる特別なオブジェクトを表現する方法としても使用されます。「式ツリー」に関する事項は次回に扱います。

ここではサンプルのため、1 つの引数と戻り値の型を例としていますが、実際には色々な引数や戻り値のパターンがあるでしょう。そして、その都度、専用のデリゲート宣言を用意するのでは、デリゲート宣言の数がやみくもに増えてしまいます。そのため、既存のクラス ライブラリなどでは、以下に示すように、前回取り上げた汎用的なジェネリック デリゲートを使用しています。

例 7. 汎用的なジェネリック デリゲートの定義

Visual Basic
Public Delegate Function Func(Of T, TResult)(ByVal arg As T ) As TResult
 

これに習って、FilterBy メソッドを書き換えると次のようになります。

例 8. ジェネリック デリゲートの利用

Function FilterBy(ByVal source As IEnumerable(Of Integer),
ByVal condition As Func(Of Integer, Boolean) ←[1]
) As IEnumerable(Of Integer)
Dim list As New List(Of Integer)
For Each dt As Integer In source
If condition(dt) Then                                        ←[6]
list.Add(dt)
End If
Next
Return list
End Function

上記の [1] に示すように、Func (Of T, TResult) デリゲートに対して、型パラメータとして、Integer 型と Boolean 型を当てはめれば、今回の条件評価を行う関数のデリゲートになります。

次に、せっかくなので、Integer 型の要素のコレクションの抽出だけでなく、様々な要素の抽出ができるように、ジェネリック メソッドに書き換えてみましょう。また、この抽出のメソッドは、コレクションに対して作用するので、そのコレクションのメンバーのように使用できると良いでしょう。よって、拡張メソッドとして構築してみましょう (拡張メソッドについては、第 9 回を参照してください)。

例 8 を次の例 9 のように変更します。

例 9. T 型要素の抽出を行う拡張メソッド

<System.Runtime.CompilerServices.Extension()>                              ←[1]
Function FilterBy(Of TSource)(ByVal source As IEnumerable(Of TSource),      ←[2]
ByVal condition As Func(Of TSource, Boolean)
) As IEnumerable(Of TSource)
Dim list As New List(Of TSource)
For Each dt As TSource In source
If condition(dt) Then
list.Add(dt)
End If
Next
Return list
End Function

上記では、拡張メソッドなので [1] のように Extension 属性が付きました。また、[2] の FilterBy メソッドという名前に続いて、"(Of TSource)" と型パラメータを定義し、さらに、今までは Integer 型だった箇所をすべて TSource 型に変更しました。

これを呼び出す場合は、次のように、より簡潔かつ直感的な記述になります。

例 10. ジェネリック メソッド、拡張メソッドを併用した簡潔な表現

Visual Basic
results = data.FilterBy(Function(x) x < 40)
 

上記では型バラメータへの型の当てはめが明記されていませんが、拡張メソッドを適用した配列 data が Integer 型要素のコレクションなので、これは IEnumerable (Of Integer) 型です。よって、TSource は Integer 型であると推論できます。(実は、拡張メソッドの適用時に型を確定する場合、他の型を当てはめることは不可能なので、明示的に "data.FilterBy (Of Integer)( ... )" と記述するとコンパイル エラーになります。)

これでラムダ式を使用する典型的な例になりました。実は、この例 10 のパターンは LINQ で頻繁に使用される表記なのです。この例 10 のもとになった、例 9 の定義をよく見てください。既に皆さんは、見たことがあるはずです。実は、第 18 回 に掲載した以下の LINQ の Where メソッドとまったく同じ形式なのです。

例 11. (再掲) Where メソッドの定義、様々なジェネリックが登場する

<ExtensionAttribute> _
Public Shared Function Where(Of TSource) ( _
source As IEnumerable(Of TSource), _
predicate As Func(Of TSource, Boolean) _
) As IEnumerable(Of TSource)

これで皆さんは、この Where メソッドの定義を見れば、どんなラムダ式を渡したらよいか導き出すことができるようになりました。


Code Recipe .NET Framework デベロッパー センター

ページのトップへ