ラムダ式をExpressionとして評価するとき、ほんの些細な記述の違いも、Expressionの中には忠実に再現される。string.StartsWithをSQL文に変換する際に、StartsWithの戻り値を明示的に判定する式を書いた。すると、明らかに異なるSQL文が生成された。たとえ意味的に同じ式であっても、Expressionでは異なる表現とみなされる。
以下に、明示的な判定を行わない式のExpressionを確認するコードを示す。
// StartsWithの戻り値をそのまま返す Expression<Func<bool>> func11 = () => "ABCDEFG".StartsWith("ABC"); // 1.ラムダ式である var lambdaExpression11 = (LambdaExpression)func11; // 2.ラムダ式の本体はメソッド呼び出しである var methodCallExpression11 = (MethodCallExpression)lambdaExpression11.Body; // 3.メソッド名はStartsWithである var methodInfo11 = methodCallExpression11.Method; Debug.Assert(methodInfo11.Name == "StartsWith"); // 4.メソッド呼び出しのインスタンスはリテラル文字列"ABCDEFG"である var instance11 = (ConstantExpression)methodCallExpression11.Object; Debug.Assert(instance11.Value.Equals("ABCDEFG")); // 5.メソッド呼び出しの引数は1個存在する var arguments11 = methodCallExpression11.Arguments; Debug.Assert(arguments11.Count == 1); // 6.その引数の式はリテラル文字列で"ABC"である var argument011 = (ConstantExpression)arguments11[0]; Debug.Assert(argument011.Value.Equals("ABC"));
ラムダ式からExpressionを得た場合、Expressionは「LambdaExpression」クラスで表現される。このクラスにはラムダ式本体を示す「Body」というプロパティがあり、これがまたExpressionを示している。上では示さなかったが、ReturnTypeやParameters(ラムダ式の引数群)を示すプロパティが定義されている。
次にBodyの中身だが、記述したラムダ式の本体は、string.StartsWithメソッドの呼び出しになっている。なので、Bodyは「MethodCallExpression」クラスで表現されている。対象のメソッドを示す「Method」を見ると、リフレクションのMethodInfoが返される。ここからメソッド名(Name)が”StartsWith”である事が分かる。
string.StartsWithはインスタンスメソッドであるので、「Object」を参照するとインスタンスを特定するExpressionが得られる。この例では”ABCDEFG”というリテラル文字列から直接メソッド呼び出ししているので、「ConstantExpression」にキャストし、「Value」プロパティを確認する事でこの文字列が得られる。
メソッドの引数は「Arguments」で、コレクションが返される。これらの一つ一つの要素もまたExpressionであるので、目的の具象クラスにキャストする必要がある。今回は引数がリテラル文字列であることが分かっているので、「ConstantExpression」にキャストして「Value」にアクセスすると、引数に指定した”ABC”が得られる。
では、明示的に判定を行う式の場合はどうなるだろうか?
// StartsWithの戻り値に、明示的な判定式を記述した場合 Expression<Func<bool>> func12 = () => "ABCDEFG".StartsWith("ABC") == true; // 1.ラムダ式である var lambdaExpression12 = (LambdaExpression)func12; // 2.ラムダ式の本体は二項演算子である var binaryExpression12 = (BinaryExpression)lambdaExpression12.Body; // 3.二項演算子の種類はイコールである Debug.Assert(binaryExpression12.NodeType == ExpressionType.Equal); // 4.二項演算子の右側はboolリテラル値でtrueである var rightExpression12 = (ConstantExpression)binaryExpression12.Right; Debug.Assert(rightExpression12.Value.Equals(true)); // 5.二項演算子の左側はメソッド呼び出しで、メソッド名はStartsWithである var leftExpression12 = (MethodCallExpression)binaryExpression12.Left; Debug.Assert(leftExpression12.Method.Name == "StartsWith");
StartsWithの中身は同じなので省略する。つまり、ラムダ式に記述した通りの構造がExpressionに再現される。無意味だからと言って、特に省略されたりすることはない。そして、LINQ to Entitiesではこの違いが、あのようなSQL文の違いとなって表れたと思われる。
同時に、ここまで解析できれば、「メソッド呼び出しの戻り値がboolである」「比較演算子を使用し、右辺(または左辺)が同じ型のリテラル」であれば、式自体を最適化可能であろうことが見えてくる。あとはどこまで実行するかということだ。