Expression Tree 上手指南 (三)

 ̄綄美尐妖づ 提交于 2020-03-21 18:41:42

上回我们说到手工解析Expression Tree,以便获得其中的逻辑或者执行我们自定义的语义动作。这种做法扩展了C#语言的威力,让我们可以用C#的语法来做更多的事情,例如Linq to Sql。今天我们要学习一种相反的做法,手工创建表达式树,然后让.NET来解析它。这是一种强大的动态编程手段。我们可以用它来完成许多以前需要Reflection.Emit才能完成的任务。

LambdaExpression的独有方法:Compile

这里我们仍要使用诸多表达式中与众不同的LambdaExpression。在Visual Studio中我们可以看到他有一个Compile方法。这是做什么的呢。在第一篇中我们了解到Lambda表达式表达的是一个方法。那么LambdaExpression.Compile方法自然就是将Lambda表达式的表达式树真的编译成一个.NET方法。我们看看这段代码:

ParameterExpression pi = Expression.Parameter(typeof(int), "i");
LambdaExpression fexp =
    Expression.Lambda(
        Expression.Add(pi, Expression.Constant(1))
        , pi);

Delegate f = fexp.Compile();

已经学过第一篇的同学一眼就能看出,这个表达式树其实就是 i => i + 1。后面的Compile方法生成了一个Delegate。如果我们试着获取这个Delegate的类型会发现他是Func<int, int>!实际上,这个委托所引用的方法是一个DynamicMethod编译后的结果。DynamicMethod是Reflection.Emit的强大功能,可在运行时动态创建出.NET方法来。我们可以正常地调用这个委托。

Expression.Lambda这个方法还有一个泛型版重载,它可以创建如同C#自己生成一样的强类型LambdaExpression。强类型LambdaExpression的Compile就更好了,能直接生成强类型的委托。这样我们调用起来就更愉快了:

ParameterExpression pi = Expression.Parameter(typeof(int), "i");
var fexp =
    Expression.Lambda<Func<int, int>>(
        Expression.Add(pi, Expression.Constant(1))
        , pi);

var f = fexp.Compile();

Console.WriteLine(f(3));

如您所预料的一样,输出是“4”。

有人可能要问了,我们干嘛不直接Expression<Func<int, int>> f = i => i + 1; 呢?从这个例子我们的确看不出什么优势。但是别忘了前面的Expression构造过程是完全可以用代码控制的,可以随意加入任何动态的逻辑。下面我们就来挑战一个用静态代码无法做到的任务:编写一个可以响应任何事件的响应函数。我们知道事件的响应函数大都是一个sender参数,一个EventArgs或其某子类的参数。但这只是.NET类库约定的常见用法,CLR完全没有规定Event的响应函数应该有几个参数,有没有返回值。假设没有ref或者out这样的参数(很少用于Event中),我们想用这样一个方法充当任何一个事件的响应函数:

static object GeneralHandler(params object[] args)
{
    Console.WriteLine("您的事件发生了说");
    return null;
}

但是,真正想要作为事件响应函数的那个方法必须真的是事件所需的委托类型。我们这样一个params参数的方法显然不能满足各色事件所需的独特委托。比如说我们要把这个事件挂在Button.Click事件上,而Button.Click事件想要的是这样一个委托:

public delegate void RoutedEventHandler(object sender, RoutedEventArgs e);

别的事件可能还需要别的委托。我们的任务就是,在运行时动态创建这一委托所需要的方法,然后调用刚才我们定义的通用响应函数,完成响应函数的挂接。这个委托的类型可以从事件的EventHandlerType中取得,然后我们再取得委托的Invoke方法即可获取它所有的参数。

接下来是关键步骤,我们希望动态创建出这样一个方法来:

//假设这是动态创建出来的一个方法
static void RoutedEventHanler_dynamic(object sender, RoutedEventArgs e)
{
    //转嫁给刚才我们定义的GeneralHandler
    GeneralHandler(sender, e);
}

这里面有一个调用params型参数的方法调用。在ExpressionTree里没有直接对应于这种调用的节点类型。我们必须还原成它编译后的底层模样,展开params参数隐式生成的数组:

//假设这是动态创建出来的一个方法
static void RoutedEventHanler_dynamic(object sender, RoutedEventArgs e)
{
    //转嫁给刚才我们定义的GeneralHandler
    //展开params调用
    GeneralHandler(new Object[] { sender, e });
}

这个语句在C#中已经完全正确了,但是如果你直接照搬翻译成ExpressionTree,就会发现它不能编译。原因是我们的GeneralHandler接受的是object类型的参数,而调用时传递的e参数是RoutedEventArgs类型的。C#能够帮你自动增加隐式类型转换,但是手写的Expression Tree可没有这么智能,得老老实实用Expression.Convert生成类型转换的表达式节点。注意,我们这里使用的事件正好是没有返回值的,95%的事件都没有返回值,不过事件的确是允许有返回值的(而且存在这样的事件,大家可以看看AppDomain类),所以我们必须区分处理有返回值和没有返回值的情况。

现在我们可以展示动态创建这一表达式树的完整代码了:(请观看注释了解代码)

public static class GeneralEventHandling
{
    static object GeneralHandler(params object[] args)
    {
        Console.WriteLine("您的事件发生了说");
        return null;
    }

    public static void AttachGeneralHandler(object target, EventInfo targetEvent)
    {
        //获得事件响应程序的委托类型
        var delegateType = targetEvent.EventHandlerType;

        //这个委托的Invoke方法有我们所需的签名信息
        MethodInfo invokeMethod = delegateType.GetMethod("Invoke");

        //按照这个委托制作所需要的参数
        ParameterInfo[] parameters = invokeMethod.GetParameters();
        ParameterExpression[] paramsExp = new ParameterExpression[parameters.Length];
        Expression[] argsArrayExp = new Expression[parameters.Length];

        //参数一个个转成object类型。有些本身即是object,管他呢……
        for (int i = 0; i < parameters.Length; i++)
        {
            paramsExp[i] = Expression.Parameter(parameters[i].ParameterType, parameters[i].Name);
            argsArrayExp[i] = Expression.Convert(paramsExp[i], typeof(Object));
        }

        //调用我们的GeneralHandler
        MethodInfo executeMethod = typeof(GeneralEventHandling).GetMethod(
            "GeneralHandler", BindingFlags.Static | BindingFlags.NonPublic);

        Expression lambdaBodyExp =
            Expression.Call(null, executeMethod, Expression.NewArrayInit(typeof(Object), argsArrayExp));

        //如果有返回值,那么将返回值转换成委托要求的类型
        //如果没有返回值就这样搁那里就成了
        if (!invokeMethod.ReturnType.Equals(typeof(void)))
        {
            //这是有返回值的情况
            lambdaBodyExp = Expression.Convert(lambdaBodyExp, invokeMethod.ReturnType);
        }

        //组装到一起
        LambdaExpression dynamicDelegateExp = Expression.Lambda(delegateType, lambdaBodyExp, paramsExp);

        //我们创建的Expression是这样的一个函数:
        //(委托的参数们) => GeneralHandler(new object[] { 委托的参数们 })

        //编译
        Delegate dynamiceDelegate = dynamicDelegateExp.Compile();

        //完成!
        targetEvent.AddEventHandler(target, dynamiceDelegate);
    }
}

如果是在一个WPF程序里,并且有一个button1的话,就可以用下列语法为button1的Click事件绑定一个我们通用的响应函数:

GeneralEventHandling.AttachGeneralHandler(button1, button1.GetType().GetEvent("Click"));

不仅仅Click,用这个语法可以放心地为任何一个.NET类型的事件绑定参数(只要没有使用ref或者out参数)。而且这里面的反射和Emit生成代码仅需要执行一次,就不会再运行了,所以即使事件多次引发也不会有性能问题。你可以充分地在GeneralHandler方法中加入自己的逻辑。

至于这个能做什么用,那就可以发挥你的想象力了。例如你可以在这个方法的帮助下实现一个跨进程的Event响应机制,就像Remoting做的那样。

目前,Expression Tree所能表达的逻辑还比较有限,尚不能完全替代Reflection.Emit。不过请放心,有LambdaExpression在,整个Expression Tree系统就是图灵完备的。理论上所有算法您都可以用Expression Tree写出来:D .NET Framework 4的Expression Tree有了更多的进化,它可以大大强化Expression Tree在这个领域的应用。敬请期待后续篇章。

习题

日常应用中我们常常会和一些数据实体类打交道。现在假设我们的数据实体类里面定义了许多public的属性。请用Expression Tree实现一个动态逻辑,可以更新任意一种数据实体类的集合,比如List<Book>, List<Employee>等等中每一个元素的指定属性。

比如我要对一个集合中所有Book元素的Author属性进行更新,您的程序就可以这样调用:

List<Book> books = …

SetAllProperty(books, “Author”, “Ninputer”);

当然您写的方法不知道Book类型,也不知道我要更新哪个属性,它应该能动态地支持任意的实体类集合。

 

(待续)

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!