表达式树(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类提供了功能可以概括为:
以上是ExpressionVisitor类的功能描述,希望朋友们可以自行阅读一下它的代码。最好还可以自行实践一番——至少可以阅读一下MSDN中的示例。
最后,我们将实现几个类,它们都实现同一个接口IExpressionCache,如下:
public interface IExpressionCache<T> where T : class { T Get(Expression key, Func<Expression, T> creator); }
接口中只有一个Get方法,如果没有对应当前key的value,那么则会通过creator委托创建一个新的value并返回。