前回のエントリでなんとなく感覚はつかんできた。
今回はライブラリ的なものに纏めてみようと思う。
まずは、Orメソッドの作成から。
Orメソッドは、Expression<Func<int, bool>> Or(Expression<Func<int, bool>> lhs, Expression<Func<int, bool>> rhs)みたいな感じになる。
早速素直に実装してみる。
static Expression<Func<int, bool>> Or(Expression<Func<int, bool>> lhs, Expression<Func<int, bool>> rhs)
{
var or = Expression.Or(lhs, rhs);
return Expression.Lambda<Func<int, bool>>(or, lhs.Parameters);
}
とまぁ、こんな素直に実装しただけじゃ動かないのが悲しいところ。
下のプログラムを実行してみると、悲しい結果に終わる。
using System;
using System.Linq.Expressions;
using System.Linq;
namespace ExpressionTreeBuildSample
{
class Program
{
static void Main(string[] args)
{
var input = Console.ReadLine();
if (input != "a" && input != "b")
{
// 入力が正しくないと終了
return;
}
// 10を表示するよ
Expression<Func<int, bool>> lambda = i => i == 10;
if (input == "b")
{
// bのときは20も表示するよ
lambda = Or(lambda, i => i == 20);
}
var nums = Enumerable.Range(1, 100);
// 検索!!
foreach (var i in nums.AsQueryable().Where(lambda))
{
Console.WriteLine(i);
}
}
static Expression<Func<int, bool>> Or(Expression<Func<int, bool>> lhs, Expression<Func<int, bool>> rhs)
{
var or = Expression.Or(lhs, rhs);
return Expression.Lambda<Func<int, bool>>(or, lhs.Parameters);
}
}
}
b
ハンドルされていない例外: System.InvalidOperationException: バイナリ演算子 Or が
型 'System.Func`2[System.Int32,System.Boolean]' と 'System.Func`2[System.Int32,S
ystem.Boolean]' に対して定義されていません。
場所 System.Linq.Expressions.Expression.GetUserDefinedBinaryOperatorOrThrow(E
xpressionType binaryType, String name, Expression left, Expression right, Boolea
n liftToNull)
場所 System.Linq.Expressions.Expression.Or(Expression left, Expression right)
場所 ExpressionTreeBuildSample.Program.Or(Expression`1 lhs, Expression`1 rhs)
場所 D:\Users\Kazuki\Document\dev\cs\WpfApplication6\ExpressionTreeBuildSample\
Program.cs:行 37
場所 ExpressionTreeBuildSample.Program.Main(String[] args) 場所 D:\Users\Kazu
ki\Document\dev\cs\WpfApplication6\ExpressionTreeBuildSample\Program.cs:行 23
続行するには何かキーを押してください . . .
どうやらOrでこけてるみたい。Expression.Orは、boolを返す式を受け取るのであってラムダ式を受け取るわけじゃないみたいだ。
ということで、ラムダ式の本体(Body)を渡すようにしてみた。
static Expression<Func<int, bool>> Or(Expression<Func<int, bool>> lhs, Expression<Func<int, bool>> rhs)
{
var or = Expression.Or(lhs.Body, rhs.Body);
return Expression.Lambda<Func<int, bool>>(or, lhs.Parameters);
}
これでもまだ駄目みたいで、下のような例外になる。
b
ハンドルされていない例外: System.InvalidOperationException: ラムダ パラメータが
スコープ内にありません
(以下略)
これは、何故出るのかというと、lhsとrhsのラムダ式のParameterExpressionが全く別物だからです。
要は下のようなコードと同じ。
// 同じiという名前だけど違うパラメータ
var param1 = Expression.Parameter(typeof(int), "i");
var param2 = Expression.Parameter(typeof(int), "i");
// 別々のパラメータと比較するようにして
var lhs = Expression.Equal(param1, Expression.Constant(10));
var rhs = Expression.Equal(param2, Expression.Constant(20));
// orをとる
var or = Expression.Or(lhs, rhs);
// そしてラムダ式へ!(パラメータはparam1のみ)
var lambda = Expression.Lambda<Func<int, bool>>(
or, param1);
// デリゲートにしてみる
lambda.Compile();
param2がlambdaの中に無いということ。
これを解決するにはInvocationExpressionを使うことでイケル。
つまり、rhsをラムダ式化してparam1を引数にして呼び出すという処理を書いてやる。
// 同じiという名前だけど違うパラメータ
var param1 = Expression.Parameter(typeof(int), "i");
var param2 = Expression.Parameter(typeof(int), "i");
// 別々のパラメータと比較するようにして
var lhs = Expression.Equal(param1, Expression.Constant(10));
var rhs = Expression.Equal(param2, Expression.Constant(20));
// rhsラムダ式化してをparam1を引数にして呼び出す
var temp = Expression.Invoke(Expression.Lambda<Func<int, bool>>(rhs, param2), param1);
// lhsとtempのorをとる
var or = Expression.Or(lhs, temp);
// そしてラムダ式へ!(パラメータはparam1のみでOK)
var lambda = Expression.Lambda<Func<int, bool>>(or, param1);
// デリゲートにしてみる
lambda.Compile();
これで例外が出なくなった!!さっそくOrメソッドにも同様の処理を書いてみる。
static Expression<Func<T, bool>> Or<T>(Expression<Func<T, bool>> lhs, Expression<Func<T, bool>> rhs)
{
// 左辺値のラムダ式のパラメータを取り出す
var param = lhs.Parameters.ToArray();
// 右辺値のラムダ式を、左辺値のパラメータで呼び出す式を作る
var invoke = Expression.Invoke(rhs, param);
// 左辺値のラムダ式の本体と、invokeのOr
var or = Expression.Or(lhs.Body, invoke);
// ラムダ式化!
return Expression.Lambda<Func<T, bool>>(or, param);
}
この状態でさっきのサンプルを動かしてbを入力すると10と20が表示された。
めでたしめでたし。同じ要領でandもいけるね。