ラムダ式の動的な構築
ラムダ式を記述するには、Functionキーワードを使用し、入力パラメータを指定し(各パラメータのデータ型も指定できます)、値を返すメソッド本体を記述します。ラムダ式の構成要素を求めるには、式をExpression(Of Delegate)
型のインスタンスに割り当てます。ラムダ式を動的に構築したい(さまざまな要素をばらばらに求めたい)場合は、関数型構築を使用して、式の各要素を表すExpressionオブジェクトを定義します。動的なラムダ式を実行するには、Expression.Compile
メソッドを呼び出します。コンパイルによって式ツリーのインスタンスがデリゲートとして返され、そのデリゲートを呼び出すことができます。
System.Linq.Expressions.Expression
クラスは、すべての式要素の基本クラスです。さまざまな式要素(例えば定数など)を構築する各種の関数メソッドは、Expression
クラスの共有メソッドです。リスト1では、Function(i) i < 33
というラムダ式を動的に構築し、この式ツリーの状態をLINQクエリでダンプし、式をコンパイルして呼び出しています。
Imports System.Collections.Generic Imports System.Linq Imports System.Linq.Expressions Imports System.Reflection Module Module1 Sub Main() ' doesn't do name look up on i so we have to use the same parameter ' expression to associate the input parameter with the parameter in ' the lambda body Dim param As ParameterExpression = Expression.Parameter(GetType(Integer), "i") Dim exp As LambdaExpression = Expression.Lambda(Of Func(Of Integer, Boolean))( Expression.LessThanOrEqual( param, Expression.Constant(33)), param ) Dim mask As String = "{0} : {1}" Dim results = From prop In exp.GetType().GetProperties() Select String.Format(mask, prop.Name, prop.GetValue(exp, Nothing)) For Each item In results Console.WriteLine(item) Next Dim lambda As Func(Of Integer, Boolean) = CType(exp.Compile(), Func(Of Integer, Boolean)) Console.WriteLine("{0}: {1}", exp.Body, lambda(10)) Console.ReadLine() End Sub End Module
このコード例は、Microsoft Visual Studio 2010で作成しています。この例を見てまず気付くのは、VB2010では行継続文字がもう必要ないということです(これは見逃せませんね)。もう1つは、ParameterExpression
が単独で定義されているという点です。ParameterExpression
は、入力引数i
と、この引数の関数本体での使用位置を表します。これは、ラムダ式の引数を使用する際に知っておく必要のある特別な「トリック」です(ただし、ヘルプファイルにはこのことについてほとんど記述がありません)。実は、パラメータ名は情報提供のためだけに存在しています。ラムダ式の実行時には、名前によるルックアップは行われません。しかし、メソッド本体のコンストラクタとパラメータ部で同じ引数param
を使用することで、式コンパイラによってパラメータと、そのパラメータのメソッド本体での使用位置が結び付けられます。もしも、式ツリーを定義するときに、次のようにメソッド本体と入力パラメータに別々の呼び出しを使用した場合は、
Dim exp As LambdaExpression = Expression.Lambda(Of Func(Of Integer, Boolean))( Expression.LessThanOrEqual( Expression.Parameter(GetType(Integer), "i"), Expression.Constant(33)), Expression.Parameter(GetType(Integer), "i") )
コンパイルされた式を実行しようとすると、InvalidOperationException
が発生します(図1を参照)。i
パラメータが存在することを式が示していても、この式はエラーになります。なぜなら、パラメータをルックアップするときには、パラメータ名ではなくパラメータの推定が使われるからです。
この例のLambdaExpression
オブジェクトは4つの部分から成ります。二項式LessThanOrEqual
と、この式に含まれるParameterExpression
式およびConstant
式、さらにExpression.Lambda
関数の最後の引数である入力パラメータです。このLambdaExpression
オブジェクトの生成後は、この式ツリーが基本的にラムダ式Function(i) i < 33
を格納することになります。
Dim results
で始まるステートメントでは、LINQクエリを使用してオブジェクトの状態ダンパーを定義しています。この状態ダンパーとFor Each
ループによって、LambdaExpression
オブジェクトの構成要素がコンソールに送られます(図2を参照)。
Dim lambda as Func(of Integer, Boolean)
ステートメントでは、LambdaExpession.Compile
(コードではexp.Compile
)の戻り値を受け取ります。簡単に言えば、ラムダ式ツリーはコンパイルされた後、ローカルのデリゲート変数に代入されて、呼び出し待ちの状態になります。最後の2行で動的なラムダ式を呼び出し、結果を表示し、ユーザーの[Enter]キー入力を待ちます。
まとめ
この記事を書くまでは、私はパラメータ式の文字列の名前が情報提供のためだけに存在しているとは知りませんでした。Anders Hejjlsbergからのブログの返事を読んで、その理由に納得しました(もっとも、ヘルプファイルではこの件についてほとんど触れられていませんが)。ここで、Andersの返事の一部を紹介します。「式内で使用されるパラメータは、オブジェクトIDを通じて参照されます。名前の比較によって参照されるわけではありません。実際、式ツリーの観点からすれば、パラメータ名は単なる情報です。このような設計になっている理由は、型がSystem.Type
を通じて参照されるのと同様です。ここでも名前は使用されません。式ツリーは完全にバインドされており、名前のルックアップルールの実装とは関係がないのです(これは言語によっては異なるかもしれません)。」というわけで、本番環境用のラムダ生成プログラムを自作することがないとしても、物事の仕組みとその理由をより深く知ることで、予期しないニーズにもプロフェッショナルとして対応できるようになるでしょう。