GTLC全球技术领导力峰会·上海站,首批讲师正式上线! 了解详情
写点什么

表达式即编译器

2009 年 9 月 22 日

介绍:问题所在

无论你是否喜欢反射,很多情况下你不可避免地会需要在运行时(而不是编译时)访问一个类型的成员。可能你在尝试着编写一些验证、序列化或是 ORM 代码,也可能必要的属性或方法是在运行时从配制文件或数据库中获得的。无论是什么原因,你在某些时候一定写过GetType() ——就像这样:

复制代码
<span color="#008000">Type</span> type = obj.GetType();
<span color="#0000ff">foreach</span> (<span color="#0000ff">var</span> property in type.GetProperties())
{
<span color="#008000">Console</span>.WriteLine("{0} = {1}",
{1}
property.Name,
{1}
property.GetValue(obj, <span color="#0000ff">null</span>));
{1}
}
{1}

虽说这个普通的示例可以正常工作,但它不够理想——这里有一些关键性的问题:

  • 它比较慢——你偶尔使用的话还好,但是在密集的循环中就会有明显的差距了。
  • 没有比较方便的做法把那些准备工作“打包”成可复用的代码。

而且,在动态代码的需求变复杂时,情况则会更加糟糕。

当然,一个明显的应对方式是“不要使用反射”——例如,手动(或使用代码生成工具)为每个类型写一个方法来做对应的事情。表面看来这种简单的做法没有问题,但是这会导致大量的重复代码,而且也无助于我们使用编译期间还不可知的类型。

我们需要的是某种形式的元编程(meta-programming)API。幸运的是,在.NET 2.0 中提供 Reflection.Emit 来动态编写 IL(Java“二进制码(bytecode)”在.NET 中的对应物),不过这也要求开发人员直接处理所有细微的装箱、类型转换、调用堆栈细节或操作符“提升”等操作——简而言之,你需要了解大量本不需要知道的 CLI 内容。另一个选择是 CodeDom,不过这也是一种显式的代码生成方式,我们想要的做的事情往往会淹没在大量构造 CodeDom 所需要的繁琐事务中。

我们需要的是一种折衷的方案,一种足够高的抽象让我们不必关心实际的 IL 如何,但也不能过于高级:我们的代码想要尽可能的简单并富于表现力。

背景:走近 System.Linq.Expressions.Expression

微软在.NET 3.5 中引入了 LINQ。LINQ 的关键部分之一(尤其是在访问数据库等外部资源的时候)是将代码表现为表达式树的概念。这种方法的可用领域非常广泛,例如我们可以这样筛选数据:

复制代码
<span color="#0000ff">var</span> query = <span color="#0000ff">from</span> <span color="#000000">cust</span> <span color="#0000ff">in</span> customers
<span color="#0000ff">where</span> cust.Region == "North"
<span color="#0000ff">select</span> cust;

虽然从代码上看不太出彩,但是它和下面使用 Lambda 表达式的代码是完全一致的:

<span color="#0000ff">var</span> query = customers.Where(cust => cust.Region == "North");LINQ 以及 Where 方法细节的关键之处,便是 Lambda 表达式。在 LINQ to Object 中,Where 方法接受一个 Func<T, bool> 类型的参数——它是一个根据某个对象(T)返回 true(表示包含该对象)或 false(表示排除该对象)的委托。然而,对于数据库这样 的数据源来说,Where 方法接受的是 Expression<Func<T, bool>> 参数。它是一个表示测试规则的表达式树,而不是一个委托。

这里的关键点,在于我们可以构造自己的表达式树来应对各种不同场景的需求——表达式树还带有编译为一个强类型委托的功能。这让我们可以在运行时轻松编写 IL。

那么,什么是一个表达式树?

与一般情况下 C#编译得到的 IL 不同,一个 Lambda 表达式会被编译为一个表现代码逻辑的对象模型。于是,例如数据库提供者(provider)便可以观察这个对象模型,理解我们编写代码的目的,在必要时便可以将这种目的转化为其他形式(如 T-SQL)。

我们不妨独立观察先前的 Lambda 表达式:

复制代码
<span color="#008000">Expression<Func<Customer</span>, <span color="#0000ff">bool</span>>> filter =
cust => cust.Region == "North";

如果要观察编译器做的事情,我们需要如平时般编译示例代码(Lambda 表达式),然后在强大(且免费)的 Reflector 中查看——不过在此之前,我们要将.NET 3.5 (C# 3.0) 优化选项关闭。

然后观察反编译器的输出内容,就会发现一些原本应该由我们自己编写的 C#代码:

请注意,虽然编译器直接访问了 MemberInfo 对象,并且使用了非法的变量名——所以你无法直接让这些输出编译通过,它只是用来参考的。有趣的是,事 实上 C#语言规范中并没有指明编译器是如何将代码转化为表达式树的,因此,使用编译结果作为参考实现,是为数不多的可用于研究表达式树的方法之一。

为了研究表达式树的手动编写方法,我们需要把相等性判断(==)和成员访问(.)视为接受运算对象的普通方法:

复制代码
<span color="#008000">Expression<Func<Customer</span>, <span color="#0000ff">bool</span>>> filter =
cust => Equal(Property(cust,"Region"),"North");

我们现在可以构建一个相同的,接受 Customer 类型作为参数,判断其 Region 属性是否为字符串“North”,并返回一个布尔值的表达式树。

复制代码
<span color="#808000">// declare a parameter of type Customer named cust</span>
<span color="#008000">ParameterExpression</span> cust = <span color="#008000">Expression</span>.Parameter(
<span color="#0000ff">typeof</span>(<span color="#008000">Customer</span>), "cust");
<span color="#808000">// compare (equality) the Region property of the<p>// parameter against the string constant "North"</p></span>
<span color="#008000">BinaryExpression</span> body = <span color="#008000">Expression</span>.Equal(
<span color="#008000">Expression</span>.Property(cust, "Region"),
<span color="#008000">Expression</span>.Constant("North", <span color="#0000ff">typeof(string</span>)));
<span color="#808000">// formalise this as a lambda<br></br></span>
<span color="#008000">Expression<Func<Customer</span>, <span color="#0000ff">bool</span>>> filter =
<span color="#008000">Expression</span>.Lambda<<span color="#008000">Func<Customer</span>, <span color="#0000ff">bool</span>>>(body, cust);

最后的“Lambda”语句是将表达式树标准化为一个完全独立的单元,并包含一系列(这个例子中只有一个)用于表达式树内部的参数。如果我们传入一个 Lambda 内不存在的参数,则会抛出一个异常。

一个表达式树,其实只是一个表现我们意图的不可变的对象模型:

  • filter (lambda)
    • parameter: Customer: “cust”
    • body (binary)
      • method: equals
      • left (member)
        - member: “Region”
        - expression: “cust” parameter
      • right (constant)
        - string: “North”

在获得一个完整的表达式树之后,我们可以将其交由 LINQ 提供者处理(此时它就会被真正当作一颗“树”来处理了),或者把它编译为一个委托——即动态生成所需的 IL:

复制代码
Func<Customer, bool> filterFunc = filter.Compile();

现在我们向委托对象中传入一个 Customer 实例并返回一个布尔值。

这看上去有些麻烦,但已经远比使用 Reflection.Emit 的方式来的简单了,尤其在一些较为复杂的情况下(如装箱)。

重要:将一个表达式树编译为委托对象涉及到动态代码的生成。如果想要获得最佳的性能,你应该仅编译一次——将其储存起来(如放在一个字段中)并复用多次。

Expression 的妙用

到目前为止,我们只是观察了一个用 C#就能完成的简单示例——这自然没什么大不了的。所以我们现在来看一些 C#无法做到的东西……我经常被人问及,有什么 方法可以编写一个通用的“加法”操作,可以对任意类型进行计算。简而言之:没有这样的语法。不过我们可以使用 Expression 来做到这一点。

简单起见,我使用 var 关键字来代替显式变量类型,我们现在将要观察如何简单地缓存可重用的委托对象:

复制代码
<span color="#0000ff">public static class</span> <span color="#008000">Operator</span><T>
{
<span color="#0000ff">private static readonly</span> <span color="#008000">Func</span><T, T, T> add;
<span color="#0000ff">public static</span> T Add(T x, T y)
{
<span color="#0000ff">return</span> add(x, y);
}
<span color="#0000ff">static</span> Operator()
{
<span color="#0000ff">var</span> x = <span color="#008000">Expression</span>.Parameter(<span color="#0000ff">typeof</span>(T), "x");
<span color="#0000ff">var</span> y = <span color="#008000">Expression</span>.Parameter(<span color="#0000ff">typeof</span>(T), "y");
<span color="#0000ff">var</span> body = <span color="#008000">Expression</span>.Add(x, y);
add = <span color="#008000">Expression</span>.Lambda<<span color="#008000">Func</span><T, T, T>>(
body, x, y).Compile();
}
}<t></t>

因为我们在静态构造函数中编译委托对象,这样的操作只会为每个不同的类型 T 执行一次,接着便可重用了——所以对于任何代码我们现在都可以直接使用 Operator。有趣的是,这个简单的“Add”方法隐藏了许多复杂性:值类型或引用类型、基础操作(直接有 IL 指令与之对应)、隐 藏操作符(方法为类型定义的一部分)以及提升操作(用于自动处理 Nullable)等等。

这个做法甚至支持用户自定义类型——只要你为它定义了 + 运算符。如果没有合适的运算符,这段代码便会抛出异常。在实际使用过程中这不太会是个问题,如果你不放心的话,可以在 Add 外加上 try 语句进行保护。

MiscUtil 类库提供了完整的通用操作符实现。

Expression 支持哪些东西?

在.NET 3.5 中,Expression 支持完整的用于查询或创建对象的操作符。

  • 算术运算:Add, AddChecked, Divide, Modulo, Multiply, MultiplyChecked, Negate, NegateChecked, Power, Subtract, SubtractChecked, UnaryPlus
  • 对象创建:Bind, ElementInit, ListBind, ListInit, MemberBind, MemberInit, New, NewArrayBounds, NewArrayInit
  • 二进制运算:And, ExclusiveOr, LeftShift (<<), Not, Or, RightShift (>>)
  • 逻辑运算:AndAlso (&&), Condition (? :), Equal, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, NotEqual, OrElse (||), TypeIs
  • 成员访问:ArrayIndex, ArrayLength, Call, Field, Property, PropertyOrField
  • 其他:Convert, ConvertChecked, Coalesce (??), Constant, Invoke, Lambda, Parameter, TypeAs, Quote

例如,我们可以用设置公开属性的方式实现浅克隆——最简单的做法可以这样写:

复制代码
person => <span color="#0000ff">new</span> <span color="#008000">Person</span>
{
{1}
Name = person.Name,
{1}
DateOfBirth = person.DateOfBirth, ...
{1}
};

对于这种情况,我们需要使用 MemberInit 方法:

复制代码
<span color="#0000ff">private static readonly</span> Func<T, T> shallowClone;
<span color="#0000ff">static</span> Operator()
{
<span color="#0000ff">var</span> orig = <span color="#008000">Expression</span>.Parameter(<span color="#0000ff">typeof</span>(T), "orig");
<span color="#808000">// for each read/write property on T, create a <p> // new binding (for the object initializer) that </p><p> // copies the original's value into the new object</p></span>
<span color="#0000ff">var</span> setProps = <span color="#0000ff">from</span> prop <span color="#0000ff">in typeof</span>(T).GetProperties (
<span color="#008000">BindingFlags</span>.Public | <span color="#008000">BindingFlags</span>.Instance)
<span color="#0000ff">where</span> prop.CanRead && prop.CanWrite
<span color="#0000ff">select</span> (<span color="#008000">MemberBinding) Expression</span>.Bind(
prop, <span color="#008000">Expression</span>.Property(orig, prop));
<span color="#0000ff">var</span> body = <span color="#008000">Expression</span>.MemberInit( <span color="#808000">// object initializer</span>
<span color="#008000">Expression</span>.New(<span color="#0000ff">typeof</span>(T)), <span color="#808000">// ctor <br></br></span>
setProps <span color="#808000">// property assignments </span>
);
shallowClone = <span color="#008000">Expression</span>.Lambda<<span color="#008000">Func</span><T, T>>(
body, orig).Compile();
}

这种做法比标准的反射方式的性能要高的多,而且不需要为每种类型维护一段代码,也不会遗漏一些新增的属性。

类似的方法还可以用于其他一些方面,如在 DTO 对象之间映射数据,在类型与 DataTable 对象之间建立关系——或比较两个对象是否相等:

复制代码
(x,y) => x.Name == y.Name &&
x.DateOfBirth == y.DateOfBirth && ...;

在这里我们需要使用 AndAlso 来结合每个操作:

复制代码
<span color="#0000ff">private static readonly</span> <span color="#008000">Func</span><T, T, <span color="#0000ff">bool</span>> propertiesEqual;
<span color="#0000ff">static</span> Operator()
{
<span color="#0000ff">var</span> x = <span color="#008000">Expression</span>.Parameter(<span color="#0000ff">typeof</span>(T), "x");
<span color="#0000ff">var</span> y = <span color="#008000">Expression</span>.Parameter(<span color="#0000ff">typeof</span>(T), "y");
<span color="#0000ff">var</span> readableProps =
<span color="#0000ff">from</span> prop in <span color="#0000ff">typeof</span>(T).GetProperties(
<span color="#008000">BindingFlags</span>.Public | <span color="#008000">BindingFlags</span>.Instance)
<span color="#0000ff">where</span> prop.CanRead
<span color="#0000ff">select</span> prop;
<span color="#008000">Expression</span> combination = <span color="#0000ff">null</span>;
<span color="#0000ff">foreach</span> (<span color="#0000ff">var</span> prop <span color="#0000ff">in</span> readableProps)
{
<span color="#0000ff">var</span> thisPropEqual = <span color="#008000">Expression</span>.Equal(
<span color="#008000">Expression</span>.Property(x, prop),
<span color="#008000">Expression</span>.Property(y, prop));
<span color="#0000ff">if</span> (combination == <span color="#0000ff">null</span>)
<span color="#808000"> { // first</span>
combination = thisPropEqual;
}
<span color="#0000ff">else</span>
<span color="#808000">{ // combine via &&</span>
combination = <span color="#008000">Expression</span>.AndAlso(
combination, thisPropEqual);
}
}
<span color="#0000ff">if</span> (combination == <span color="#0000ff">null</span>)
<span color="#808000">{ // nothing to test; return true</span>
propertiesEqual = <span color="#0000ff">delegate { return true</span>; };
}
<span color="#0000ff">else</span>
{
propertiesEqual = <span color="#008000">Expression</span>.Lambda<<span color="#008000">Func</span><T, T, bool>>(
combination, x, y).Compile();
}
}

Expression 的限制——及展望

到目前为止,Expression 的表现几近完美——然而,还有 LINQ 表达式的构造方式还有许多明显的限制:

  • 没有内置的机制可以改变对象的属性 / 字段。
  • 没有方法可以执行一系列的操作

在上面的例子中,我们可以使用 AndAlso 将多个步骤连接成一个逻辑语句。然而,在.NET 3.5 中无法使用一个表达式树来表现如下的语句:

复制代码
person.DateOfBirth = newDob;
person.Name = newName;
person.UpdateFriends();
person.Save();

我们之前看到的 Bind 操作只能在创建新对象时使用。我们可以获取 setter 方法,但是我们无法将多个方法调用串联起来(就像“流畅”API 那样,可惜目前没有)。

幸运的是,.NET 4.0 扩展了 Expression API,增加了新的类型和方法。这么做的目的是支持动态语言运行时(DLR,Dynamic Language Runtime)。这大大扩展了:

  • 修改操作:AddAssign, AddAssignChecked, AndAssign, Assign, DivideAssign, ExclusiveOrAssign, LeftShiftAssign, ModuloAssign, MultiplyAssign, MultiplyAssignChecked, OrAssign, PostDecrementAssign, PostIncrementAssign, PowerAssign, PreDecrementAssign, PreIncrementAssign, RightShiftAssign, SubtractAssign, SubtractAssignChecked
  • 算术操作:Decrement, Default, Increment, OnesComplement
  • 成员访问:ArrayAccess, Dynamic
  • 逻辑运算:ReferenceEqual, ReferenceNotEqual, TypeEqual
  • 逻辑流:Block, Break, Continue, Empty, Goto, IfThen, IfThenElse, IfFalse, IfTrue, Label, Loop, Return, Switch, SwitchCase, Unbox, Variable
  • 异常操作:Catch, Rethrow, Throw
  • 调试:ClearDebugInfo, DebugInfo

这对编写动态代码来说可谓是个天大的好消息(它甚至包含了将代码编译为 MethodBuilder 的能力,可生成动态类型)。例如,我们可以用它来编写一个简单的 for 循环来打印数字 0 到 9:

复制代码
<span color="#0000ff">var</span> exitFor = <span color="#008000">Expression</span>.Label("<span color="#ff0000">exitFor</span>"); <span color="#808000">// jump label<br></br></span>
<span color="#0000ff">var</span> x = <span color="#008000">Expression</span>.Variable(<span color="#0000ff">typeof(int</span>), "<span color="#ff0000">x</span>");
<span color="#0000ff">var</span> body = <span color="#008000">Expression</span>.Block(<span color="#0000ff">new</span>[] { x }, <span color="#808000">// declare scope variables<br></br></span>
<span color="#008000">Expression</span>.Assign(x,
Expression.Constant(0, <span color="#0000ff">typeof(int</span>))), <span color="#808000">// init<br></br></span>
<span color="#008000">Expression</span>.Loop(
<span color="#008000">Expression</span>.IfThenElse(
<span color="#008000">Expression</span>.GreaterThanOrEqual( <span color="#808000">// test for exit</span>
x,
<span color="#008000">Expression</span>.Constant(10, <span color="#0000ff">typeof(int</span>))
),
<span color="#008000">Expression</span>.Break(exitFor), <span color="#808000">// perform exit<br></br></span>
<span color="#008000">Expression</span>.Block( <span color="#808000">// perform code</span>
<span color="#008000">Expression</span>.Call(
<span color="#0000ff">typeof</span>(<span color="#008000">Console</span>), "<span color="#ff0000">WriteLine</span>", <span color="#0000ff">null</span>, x),
<span color="#008000">Expression</span>.PostIncrementAssign(x)
)
), exitFor));
<span color="#0000ff">var</span> runtimeLoop = <span color="#008000">Expression</span>.Lambda<<span color="#008000">Action</span>>(body).Compile();

虽然看上去似乎不是那么一鸣惊人,但如果您要在运行时编写编译好的代码,它比其他方式要方便和灵活得多。

之前我们通过属性复制来克隆一个对象——不过现在我们已经有能力把一个对象的数据复制给另一个对象了,这点对于 ORM 工具来说非常有用,例如跟踪一个对象的状态,执行外部更新,再提交这些改变。我们可以这么做:

复制代码
<span color="#0000ff">var</span> source = <span color="#008000">Expression</span>.Parameter(<span color="#0000ff">typeof</span>(T), "<span color="#ff0000">source</span>");
<span color="#0000ff">var</span> dest = <span color="#008000">Expression</span>.Parameter(<span color="#0000ff">typeof</span>(T), "<span color="#ff0000">dest</span>");
<span color="#808000">// for each read/write property, copy the source's value<p> // into the destination object</p></span>
<span color="#0000ff">var</span> body = <span color="#008000">Expression</span>.Block(
<span color="#0000ff">from</span> prop <span color="#0000ff">in typeof</span>(T).GetProperties(
<span color="#008000">BindingFlags</span>.Public | <span color="#008000">BindingFlags</span>.Instance)
<span color="#0000ff">where</span> prop.CanRead && prop.CanWrite
<span color="#0000ff">select</span> <span color="#008000">Expression</span>.Assign(
<span color="#008000">Expression</span>.Property(dest, prop),
<span color="#008000">Expression</span>.Property(source, prop)));
<span color="#0000ff">var</span> copyMethod = <span color="#008000">Expression</span>.Lambda<<span color="#008000">Action</span><T,T>>(
body, source, dest).Compile();

使用表达式在运行时创建类型

.NET 4.0 中有个有趣的功能:在此之前我们可以把一个 Expression 编译为一个独立的委托,而如今你可以在动态创建新类型时,使用 CompileToMethod 方法将一个表达式树作为方法体提供给一个 MethodBuilder 对象。这意味着,在未来我们可以方便地编写功能丰富的 类型(包括多态和接口实现),而不用直接接触 IL。

语言中的高级表达式

尽管运行时已经支持非常成熟的表达式树了,但是在 C# 4.0 中并没有额外的语言支持——所以我们只能手动创建表达式。这一般不会成为问题,因为几乎没有 LINQ 提供者会支持这些复杂表达方式。如果你在编译期 知道这些类型,不如直接编写普通的方法或匿名方法。不过这也为 4.0 以后的版本留下了想象空间。例如(只是推测),想象一下如果语言可以使用 Expression.Assign 和 Expression.Block 为数据库构造一个表达式树,就好比:

复制代码
<span color="#808000">// imaginary code; this doesn’t work</span>
myDatabase.Customers
.Where(c => c.Region == "<span color="#ff0000">North</span>")
.Update(c => {
{1}
c.Manager = "<span color="#ff0000">Fred</span>";
{1}
c.Priority = c.Priority + 10;
{1}
});

再次强调这只是“假如”——这需要在 LINQ 提供者上耗费大量的工作,但是我们从中可以看出:理论上说,一个 Expression 对象是有能力来封装我们的意图的(更新实体的多个属性)。可惜,无论是语言还是框架都还有很长的路要走,只有时间可以说明一切。

阅读英文原文 Expression as a Compiler


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2009 年 9 月 22 日 10:185374
用户头像

发布了 157 篇内容, 共 44.2 次阅读, 收获喜欢 2 次。

关注

评论

发布
暂无评论
发现更多内容

你以为你真的理解 Closure 吗

大导演

Java 前端进阶训练营

tcpdump 实例-获取网络包的50种方法

Rayjun

TCP/IP tcpdump

我成功转行做了java程序猿!

诸葛小猿

Java 转行程序员 转行

上班摸鱼,可以玩一整天,哈哈哈!!!

诸葛小猿

上班 摸鱼

编程核心能力之复用

顿晓

编程 复用 编程日课 技术思维

小白教程——基于阿里云快速搭建自己的网站

诸葛小猿

阿里云 视频 网站搭建 小白

图解:最短路径之如何理解“松弛”or“放松”?

淡蓝色

Java 数据结构 算法

Week7 作业

Shawn

第7周笔记:性能优化

Melo

普本毕业三年,四面华为,因精通这6大知识点拿到25*16薪offer

互联网架构师小马

Java 程序员 面试 求职 找工作

分布式系统信息一致性问题与方案分析

superman

分布式 极客大学架构师训练营

LeetCode题解:141. 环形链表,JavaScript,快慢指针,详细注释

Lee Chen

LeetCode 前端进阶训练营

ARTS Week7

丽子

ARTS 打卡计划

IDEA命令行缩短器助你解决此问题:Command line is too long. Shorten command line...

YourBatman

intellij-idea spring IDEA springboot

LeetCode题解:1051. 高度检查器,JavaScript,桶排序,详细注释

Lee Chen

LeetCode 前端进阶训练营

WordPress插件设计

心平气和

php 插件设计 插件系统 WordPress

程序员都应该知道的数据库避坑指南

Phoenix

MySQL 数据库 事务隔离级别

Discuz插件设计

心平气和

php Diszuz 插件设计 插件系统

第7周作业:web性能测压工具

Melo

写一个并发测试工具

罗亮

CAP原理

Arvin

全球区块链专利排行榜中国52家企业上榜

CECBC区块链专委会

Rust多线程之数据共享

编号94530

rust 多线程 数据共享 什么是多线程

Java 基础知识整理

多选参数

Java

不变的是什么?

zhongzhq

依道而行 规律 变化

raft协议中, 候选人角色能参与投票吗

程序员老王

raft

Flink 生态:Pulsar Connector 机制剖析

Apache Flink

flink

浪潮信息推动AI在线教育实现全面应用

Geek_116789

第七周作业

田振宇

架构师训练营第六周-总结

人世间

负载均衡+分布式数据库

Arvin

DNSPod与开源应用专场

DNSPod与开源应用专场

表达式即编译器-InfoQ