Hello World
Spiga

谈表达式树的缓存(1):引言

2009-03-16 09:29 by 老赵, 13765 visits

表达式树(Expression Tree)是.NET 3.5中引入的一种表达方式。表达式树的运用十分广泛,可以直观地表现出各种“数据”,甚至“逻辑”和“行为”。再者,表达式树是强类型的,因此合理地使用这个新特性可以让代码编写变得优雅,方便。一个最简单而常见的例子便是,某些朋友目前就已经喜欢使用表达式树来代替传统的ByXxx方法,尤其是在访问一些直接支持表达式树的数据源时(例如IEnumerable或LINQ to SQL)。如下:

public User GetUser(Expression<Func<User, bool>> predicate) { }

而不必写成:

public User GetUserByID(int id) { }
public User GetUserByName(string name) { }

于是在调用时便可以:

var user1 = GetUser(u => u.UserID == 1);
var user2 = GetUser(u => u.Name == "jeffz");

姑且不论这种设计方式是否合适(因为即使这个做法不合理,也不能代表所有的用法),我们先达成一个共识,那就是“表达式树很有用”——于是我们的接下去话题看上去才会比较有价值:P。那么就上面一个问题来说,在使用了表达式树的情况下,如何在方法中进行缓存?在ByID或ByName的情况下,我们可以轻易地构造一些字符串作为缓存的键,例如“GetUserByID_100”或“GetUserByName_jeffz”。但是现在呢?每次在调用时就会生成一个不同的Expression对象,就算大家“表现一致”,也无法被“识别为”同样的对象,而直接用作缓存的键,因此处理起来并不是那么直接了当的。

您可能会说,那么就在解析表达式树的时候,识别出它是ByID还是ByName,然后再拼接出之前的字符串。当然,您如果真的是要解决上面这个例子的问题,那么的确可以用这种方法。但是老赵现在希望可以找到一种较为通用的,能够根据表达式树进行缓存的解决方案——事实上老赵本来就是在设计一个通用的功能时才引发了这个需求,而这个功能也打算在详细谈完缓存问题后与大家共享。

这个缓存问题看上去简单,但是实际上在性能和功能进行权衡之后会有多种策略可以选择。老赵会在这里谈论5种缓存策略,它们各有千秋,有的方式资源很省,性能很好;而有的方式从性能上比较落后,资源占用也相对较高,但是在某些场景下它似乎还是唯一的解决方案。因此,至少我觉得讨论一下这个问题也是非常有意思的事情,而且从一定程度上说,这些思考能够在一定程度上体现出算法设计与数据结构的美妙之处(尽管相对来说它们其实非常简单)。

在这一系列文章中,老赵希望可以重现自己在思考这个问题的时候所形成的完整思考路径。相比最终解决方案,这可能才是更有价值的东西。文章有时也会将朋友们“引入歧途”,其目的也是为了让弟兄们一起经历一下老赵走过的弯路。到了最后,您可以会说“这死胖子真笨,怎么早没想到”(呵呵,大家莫怪)。此外,这5种缓存策略也并非是思考的全部,事实上老赵相信还会有更好的解决方案(至少理论上是这样的),而由于种种原因并没有在这里实现出来。因此,老赵也希望大家在看了文章之后可以一起思考,并谈出您的看法。:)

不过,表达式树的“构造”很简单,我们可以使用Lambda表达式轻松地生成一颗表达式树,但是在具体操作时就较为困难了。因此在理解这一系列文章之前,可能您还需要作一些准备,也就是一些基础的,操作表达式树的方式。在操作表达式树时,必不可少的东西便是一个ExpressionVisitor类,您可以在MSDN中找到其实现及相关示例,几乎任何操作表达式树的类都会继承于它。总体来说,ExpressionVisitor类提供了功能可以概括为:

  1. Visit方法接受一个Expression类型的参数,并根据Expression的具体类型委托给特定的VisitXxx方法进行访问,并将结果返回。
  2. 各VisitXxx方法会将得到的具体Expression类型的参数(如UnaryExpression)的各部分,即各个“子Expression”交给Visit方法进行访问,并得到其返回值。
  3. 各VisitXxx方法如果发现每个Visit方法的调用都返回了原来的“子Expression”,那么则直接返回自身得到的参数,否则就构造一个新的Expression对象并返回。

以上是ExpressionVisitor类的功能描述,希望朋友们可以自行阅读一下它的代码。最好还可以自行实践一番——至少可以阅读一下MSDN中的示例。

最后,我们将实现几个类,它们都实现同一个接口IExpressionCache,如下:

public interface IExpressionCache<T> where T : class
{
    T Get(Expression key, Func<Expression, T> creator);
}

接口中只有一个Get方法,如果没有对应当前key的value,那么则会通过creator委托创建一个新的value并返回。

 

完整代码下载:http://code.msdn.microsoft.com/ExpressionCache

相关文章:

Creative Commons License

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

Add your comment

26 条回复

  1. 流牛木马
    *.*.*.*
    链接

    流牛木马 2009-03-16 02:13:00

    好不容易坐上了老赵的沙发~
    期待后续文章!

  2. 不会[未注册用户]
    *.*.*.*
    链接

    不会[未注册用户] 2009-03-16 03:15:00

    不会表达式数

  3. wuxiaoqqqq
    *.*.*.*
    链接

    wuxiaoqqqq 2009-03-16 09:01:00

    周五看了一下,不知道到底要怎么用,等待后文。

  4. 老赵
    admin
    链接

    老赵 2009-03-16 09:32:00

    --引用--------------------------------------------------
    wuxiaoqqqq: 周五看了一下,不知道到底要怎么用,等待后文。
    --------------------------------------------------------
    后文是不会叙述表达式树是怎么用的,所以还是要自己理解的好……

  5. 老赵
    admin
    链接

    老赵 2009-03-16 09:32:00

    @不会
    不知道这个是要吃亏滴

  6. PureEviL
    *.*.*.*
    链接

    PureEviL 2009-03-16 09:58:00

    一次构建,多次使用,跟Reflection优化挺像。
    构建表达式树的成本还是挺高的
    期待下文

  7. 老赵
    admin
    链接

    老赵 2009-03-16 10:07:00

    @PureEviL
    完全不是一回事情啊,以前是根据某个key缓存Expression Tree生成的委托,注重委托的获得。
    现在是根据某个Expression Tree缓存任意value,注重怎么把两个Expression Tree识别为同一个。

  8. guozili@163.com
    *.*.*.*
    链接

    guozili@163.com 2009-03-16 10:24:00

    老赵总是研究这种高深的技术,不顶不行啊

  9. 老赵
    admin
    链接

    老赵 2009-03-16 10:28:00

    --引用--------------------------------------------------
    guozili@163.com: 老赵总是研究这种高深的技术,不顶不行啊
    --------------------------------------------------------
    谢谢,用到了就设法解决而已。

  10. wuzhihao
    *.*.*.*
    链接

    wuzhihao 2009-03-16 10:34:00

    好厉害啊!顶你

  11. 木野狐(Neil Chen)
    *.*.*.*
    链接

    木野狐(Neil Chen) 2009-03-16 10:55:00

    抱歉没有看明白,这里缓存的意思是不是想实现

    Expression<Func<User,bool>> expr1 = ..;
    var user1 = GetUser(expr1);

    Expression<Func<User,bool>> expr2 = ..;
    var user2 = GetUser(expr2);

    如果判断 expr1 和 expr2 逻辑上是等价的,则执行第二句的时候 GetUser 方法内部直接返回缓存的结果?

    是这个意思吗?

  12. 老赵
    admin
    链接

    老赵 2009-03-16 11:19:00

    @木野狐(Neil Chen)
    差不多

  13. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2009-03-16 11:33:00

    貌似你一直都很关注lamda有关的东东哦。我最近也开始看看LINQ了,在Kindle for iPhone上看Pro LINQ。

  14. 木野狐(Neil Chen)
    *.*.*.*
    链接

    木野狐(Neil Chen) 2009-03-16 11:34:00

    @Jeffrey Zhao
    谢谢。关注你后续的文章。

  15. 老赵
    admin
    链接

    老赵 2009-03-16 11:36:00

    --引用--------------------------------------------------
    Cat Chen: 貌似你一直都很关注lamda有关的东东哦。我最近也开始看看LINQ了,在Kindle for iPhone上看Pro LINQ。
    --------------------------------------------------------
    因为很有用,所以经常用,顺便就解决了遇到的问题。

  16. 老赵
    admin
    链接

    老赵 2009-03-16 11:36:00

    --引用--------------------------------------------------
    木野狐(Neil Chen): @Jeffrey Zhao
    谢谢。关注你后续的文章。
    --------------------------------------------------------
    客气,高手看了莫笑,呵呵

  17. 江大鱼
    *.*.*.*
    链接

    江大鱼 2009-03-16 11:43:00

    楼主的文章让我受益匪浅阿

  18. 老赵
    admin
    链接

    老赵 2009-03-16 12:40:00

    --引用--------------------------------------------------
    江大鱼: 楼主的文章让我受益匪浅阿
    --------------------------------------------------------
    有么……我怎么觉得我还什么都没写

  19. 0750[未注册用户]
    *.*.*.*
    链接

    0750[未注册用户] 2009-03-16 13:32:00

    继续关注 项目中正在用LINQ...........

  20. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-03-16 13:56:00

    直接用Expression<T>的ToString方法不行么?

  21. 老赵
    admin
    链接

    老赵 2009-03-16 14:04:00

    @Ivony...
    其实我始终觉得,不要猜,自己尝试一下会比较好。

  22. kyorry
    *.*.*.*
    链接

    kyorry 2009-03-16 19:21:00

    2点以后就没人回复了???
    呵呵,就让我拍在前面来了。
    老赵,一直想跟你学习,真心希望你能收个弟子

  23. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-06-16 15:25:00

    刚看了一下代码,简直如神来之笔!佩服

  24. 麦子
    58.41.17.*
    链接

    麦子 2011-11-24 21:56:53

    开始阅读两年前的系列文章,呵呵,看看“死胖子”的算法应用

  25. peijian708
    58.57.66.*
    链接

    peijian708 2012-03-22 17:08:39

    如何生成动态LambdaExpression 例如a=>a.tWorkOrder.iDDBH

  26. 老赵
    admin
    链接

    老赵 2012-03-23 16:56:13

    @peijian708

    看文档去。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我