Hello World
Spiga

使用.NET 4.0中的表达式树生成动态方法

2010-05-13 01:03 by 老赵, 8061 visits

为了在模型为dynamic类型的视图中使用一个匿名对象,我们在上一篇文章里为匿名对象创建了对应的动态类型。于是在使用时,我们会创建动态类型的对象,然后将匿名对象的属性赋值给动态对象的公开字段上。在赋值时我们使用了反射,再加上这个方法使用比较频繁,因此使用更好的方法来优化性能便是个很自然的选择。在.NET 1.0中,我们需要Emit;在.NET 2.0中则增加了DynamicMethod,相对简化了单个动态方法的创建过程;在.NET 3.5中则增加了可编译表达式树,可谓前进了一大步——那么在.NET 4.0中呢?

使用.NET 3.5中的表达式树,我们可以动态创建出一个表达式结构,然后编译成一个委托对象。这种做法我使用过很多次(比如这里这里),它的优势在于我们可以将关注点放在“表达式”层面上,而不用直接使用繁琐易错的Emit操作。更关键的是,此外,表达式树还会帮我们隐藏了自动类型转换、或是装箱/拆箱等原本需要开发人员直接关注的问题。例如,在表达式层面上,整数相加和字符串的连接都是个Add操作,但是在IL级别它们一个是Add指令,另一个却是String.Concat方法的调用。

不过.NET 3.5中的表达式树有个限制,那便它只能创建出“表达式(Expression)”,而无法创建“语句(Statement)”。例如,我们无法定义一个包含循环的方法。不过这一切在.NET 4.0里得到了改善。.NET 4.0引入了一部分原本在DLR中的更丰富的表达式树,包含变量声明、for循环、判断、GOTO跳转等各种表达式,因此我们基本上可以用它来表达任意一段完整的逻辑了。那么,它又该如何来改进上一篇文章里的性能问题呢?

上文末尾我提到,我们可以把动态类型创建成如下模样:

public class DynamicType
{
    public string Abc;
    public int Ijk;
    public bool Xyz;
    ...

    public DynamicType(object entity)
    {
        var strongTyped = (EntityType)entity;

        this.Abc = strongTyped.Abc;
        this.Ijk = strongTyped.Ijk;
        this.Xyz = strongTyped.Xyz;
        ...
    }
}

不过事实上,我们完全可以还是创建一个最为普通的动态类型:

public class DynamicType
{
    public string Abc;
    public int Ijk;
    public bool Xyz;
    ...
}

只要再配合这么一个动态生成的方法就行了:

entity =>
{
    var dynamicEntity = new DynamicType();
    var strongTyped = (EntityType)entity;

    dynamicEntity.Abc = strongTyped.Abc;
    dynamicEntity.Ijk = strongTyped.Ijk;
    dynamicEntity.Xyz = strongTyped.Xyz;
    ...

    return (object)dynamicEntity;
}

是不是很容易?而且事实上生成这么一个方法也非常简单,和以前一样,只管创建表达式树即可:

private static Func<object, object> DynamicFactoryCreator(Type entityType)
{
    var dynamicType = CreateDynamicType(entityType);

    // define the parameter
    var parameterExpr = Expression.Parameter(typeof(object), "entity");

    // collect the body
    var bodyExprs = new List<Expression>();

    // code: var dynamicEntity = new DynamicType();
    var dynamicEntityExpr = Expression.Variable(dynamicType, "dynamicEntity");
    var newDynamicTypeExpr = Expression.New(dynamicType);
    var assignDynamicEntityExpr = Expression.Assign(dynamicEntityExpr, newDynamicTypeExpr);
    bodyExprs.Add(assignDynamicEntityExpr);

    // code: var strongTyped = (EntityType)entity;
    var strongTypedExpr = Expression.Variable(entityType, "strongTyped");
    var castEntityExpr = Expression.Convert(parameterExpr, entityType);
    var assignStrongTypedExpr = Expression.Assign(strongTypedExpr, castEntityExpr);
    bodyExprs.Add(assignStrongTypedExpr);

    // generate code for fields' assignments
    foreach (var property in entityType.GetProperties())
    {
        // code: dynamicEntity.Xyz = strongTyped.Xyz
        var fieldExpr = Expression.Field(dynamicEntityExpr, property.Name);
        var propertyExpr = Expression.Property(strongTypedExpr, property);
        var assignFieldExpr = Expression.Assign(fieldExpr, propertyExpr);

        bodyExprs.Add(assignFieldExpr);
    }

    // code: return (object)dynamicEntity;
    var castResultExpr = Expression.Convert(dynamicEntityExpr, typeof(object));
    bodyExprs.Add(castResultExpr);

    // code: { ... }
    var methodBodyExpr = Expression.Block(
        typeof(object), /* return type */
        new[] { dynamicEntityExpr, strongTypedExpr } /* local variables */,
        bodyExprs /* body expressions */);

    // code: entity => { ... }
    var lambdaExpr = Expression.Lambda<Func<object, object>>(methodBodyExpr, parameterExpr);

    return lambdaExpr.Compile();
}

上面这段代码创建的完全是我们准备好的那种动态方法形式,这点从注释中也可以看出。首先,我们会使用上文定义的CreateDynamicType方法中创建一个动态类型,然后使用一个List容器收集方法体内所有的语句,再把它们交给Expression.Block方法创建一个“方法块”(而不仅仅是“表达式”),最后生成Lambda表达式,编译完成。

虽说这段代码似乎比较长,但我个人认为还是相当清晰的。我建议您可以适当把玩一下.NET 4.0中的表达式树类库,例如创建一些包含for循环,goto跳转等逻辑的方法体。根据我的经验,其实需要动态生成方法的场景似乎也真比原本我想象中要来的多。

最后,再改写原来的DynamicFactory类及ToDynamic扩展方法:

public static class DynamicFactory
{
    private static ConcurrentDictionary<Type, Func<object, object>> s_dynamicEntityFactories =
        new ConcurrentDictionary<Type, Func<object, object>>();

    private static Func<Type, Func<object, object>> s_factoryCreator =
        new Func<Type, Func<object, object>>(DynamicFactoryCreator);

    public static object ToDynamic(this object entity)
    {
        var entityType = entity.GetType();
        var factory = s_dynamicEntityFactories.GetOrAdd(entityType, s_factoryCreator);
        return factory(entity);
    }

    ...
}

好,我们的性能优化到这里就完成了。不过再回到我们原本的问题——如果我们要在ASP.NET MVC的视图中使用dynamic类型作为参数,并传递匿名对象的话,那么还能使用什么办法来避免古怪的“无法找到成员”异常呢?或者这么说,如果不像现在这样生成动态类型,那么ToDynamic方法还可以怎么实现呢?

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

26 条回复

  1. martinluo
    125.86.126.*
    链接

    martinluo 2010-05-13 01:46:36

    沙发一个~~~ 老赵怎么用markitup作为编辑器啊? 没预览的哦

  2. 链接

    童刚刚 2010-05-13 08:18:42

    这个看得云里雾里,呵呵

  3. 链接

    孝园 2010-05-13 08:29:59

    只能以后再看了。

  4. 看看
    222.92.42.*
    链接

    看看 2010-05-13 08:58:39

    留个脚印,以后再看。

  5. 老赵
    admin
    链接

    老赵 2010-05-13 09:42:07

    @martinluo

    这么明显的实时预览放在这里你还说没预览啊……

  6. back
    202.99.16.*
    链接

    back 2010-05-13 10:03:03

    高山仰止,曲高和寡

  7. baihmpgy
    124.114.10.*
    链接

    baihmpgy 2010-05-13 11:18:47

    一个字:帅!

  8. zzfff
    113.141.29.*
    链接

    zzfff 2010-05-13 11:32:51

    “语句”是statement

  9. 老赵
    admin
    链接

    老赵 2010-05-13 12:01:25

    @zzfff

    脑子抽了,多谢提醒。

  10. 链接

    麒麟.NET 2010-05-13 15:10:00

    还有什么方法呢?一时还真想不到

  11. 老菜
    61.162.217.*
    链接

    老菜 2010-05-13 16:22:29

    看起来有点吃力,要努力向老赵学习啊。 多谢老赵将这些编程心得与我们共享。

  12. 老菜
    61.162.217.*
    链接

    老菜 2010-05-13 17:05:35

    看了上一篇,熟悉了动态创建类型。 学习了这一篇,更多了解了表达式(不仅限于Lambda表达式)。 有收获。

  13. 链接

    陈梓瀚(vczh) 2010-05-14 09:55:40

    难道dynamic a=b;跟dynamic a=(object)b;的结果不一样?这怎么可以呢?

  14. 老赵
    admin
    链接

    老赵 2010-05-14 11:19:36

    @陈梓瀚(vczh)

    应该是一样的,在Console里执行是正常的。

  15. 老菜
    61.162.215.*
    链接

    老菜 2010-05-14 11:26:01

    看了这段代码,感觉几乎所有代码都可以用Expression来实现。

    用Expression可以定义变量、赋值、调用方法(包括传参)、执行算法运算、生成语句块,这些功能在老赵的例子里都用到了,这几乎包含了手写C#代码的所有功能了。

    另外,我记不太清楚了,在某人的博客里看到.NET Framework 4.0里Expression支持流程控制,即if. for等语句,这样的话,用Expressoin几乎就可以做任何C#代码能完成的工作了,很强大。

  16. 森
    124.207.206.*
    链接

    2010-05-14 11:50:58

    搜索不能用,有脚本错误

  17. 老赵
    admin
    链接

    老赵 2010-05-14 13:19:38

    @老菜

    现在的Expression的确可以表示任意逻辑,生成动态方法很方便。

  18. 老菜
    123.133.161.*
    链接

    老菜 2010-05-14 23:53:06

    老赵的博客都写的很好,但是我一时不能全部看完,挑了其中几篇我特别感兴趣的,发表了回复作为记号。我想以后来这里时还能找到这些记号,该怎么办?

    这个博客是否有这样的功能:查找我所有回复,进而找到我所有回复过的文章?

  19. Neil Chen
    210.184.115.*
    链接

    Neil Chen 2010-06-02 11:08:19

    Great post.

  20. 没有
    131.107.0.*
    链接

    没有 2010-12-28 04:11:13

    既然Expression可以表示任意逻辑了……啥时候弄个eval函数出来

  21. 老赵
    admin
    链接

    老赵 2010-12-28 07:19:38

    @没有

    早就有了。

  22. 叫啥子
    219.139.223.*
    链接

    叫啥子 2011-03-18 01:49:21

    请问一下,如果生成一个匿名类的表达式,或者说如何生成可以返回多列的Select表达式??

  23. cjy
    121.35.211.*
    链接

    cjy 2013-05-21 20:09:40

    DynamicType 加 Expression 怎么实现 : var a = "2 + 3 * 100"; //a 是动态传入的 var b = Eval(a);

  24. 老赵
    admin
    链接

    老赵 2013-05-21 23:27:11

    @cjy

    自己Parse,或搜下C# Evaluator或C# REPL之类的。

  25. linustd
    123.103.9.*
    链接

    linustd 2015-06-01 15:50:30

    卧槽,今天网上找资料比较 .net 的 SignalR 和 socket.io的区别,想查查.net的动态表达式,结果又找到这网站了。

    微软这傻逼咋整天搞这些奇技淫巧呢,整个破动态表达式,难道对程序的性能、并发、用户体验有好处?

    狗屁没有任何用处的奇技淫巧,人家那些没有动态表达式功能的语言照样工资比干.net的高,照样开发出的产品比.net的流行。

    看看这微软啥玩意啊,但是竟然还有很多傻鸟跟着微软跑,以为学会了微软的奇技淫巧就是高手了。

  26. 地山谦
    183.62.251.*
    链接

    地山谦 2015-06-17 09:11:38

    老赵,请教两个问题: Expression.MakeCatchBlock这个方法是怎么用的。微软没有提供用法示例。 特别是第三个参数,那个filter。没有头序哦。

    另外,CatchBlock类有一个:Variable,如果我要构造一个CatchBlock,那么我肯定不能用CatchBlock的引用来拿Variable,不然就循环引用了。 如果是这样,Variable最可能的使用场景在哪里呢?

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我