C#教程:编程方式构建表达式树


表达式树

“代码就是数据”是一个很古老的观念, 其并没有在很多流行的编程语言中使用. 你可以争论说所有的.NET程序都使用了这个观点, 因为JIT将所有IL代码都认为是数据, 并将他们转换成基于本机CPU的本地代码. 这隐藏得很深, 而且由于有存在的库用于操作IL代码, 因此它们并没有被广泛的应用.

.NET 3.5当中的提供了一种抽象的方式来将代码展现为一颗对象树. 这有点类似CodeDOM, 不过是在更高一层的级别上操作, 而且仅限于表达式. 表达式树的主要用处是在LINQ当中. C# 3当中对于将lambda表达式转换为表达式树提供了内建支持. 不过在我们开始讲解它们之前先来了解一下在没有编译器的帮助之下, 它们是如何适用于.NET Framework当中的.

编程方式构建表达式树

表达式树并没有听起来的那么神秘, 虽然有些时候他们的用法看起来的确有点像是魔术. 像它们的名字所展现的, 它们是对象树, 在树中的每一个节点都是其内部的一个表达式. 不同类型的表达式代表了可以在代码中执行的不同操作: 二进制操作, 例如加法; 一元操作, 例如读取数组长度; 方法调用; 构造器调用等等.

System.Linq.Expression命名空间包含了代表表达式的几个不同类. 它们全部都继承自Expression类, Expression是一个抽象的, 并且大部分由创建其他表达式实例的静态工厂方法组成. Expression类还暴露了两个属性:

  • Type属性代表了将要评估的表达式对应的.NET类型——你可以认为它是一个返回类型. 例如如果一个表达式读取了一个string的Length属性, 那么表达式的Type将为int.
  • NodeType属性返回表达式所代表的类型, 作为一个ExpressionType枚举的成员, 它可以是LessThan, Multiply和Invoke等等. 使用相同的例子, myString.Length当中, 属性访问部分将会得到一个MemberAccess的NodeType

有很多的类型都继承了Expression,其中的一部分有很多不同的节点类型(node types): 例如BinaryExpression, 代表了两个操作数之间的任何操作: 算术, 逻辑, 比较, 数组索引以及其他类似的操作. 这就是为什么NodeType在这里这么重要的原因, 因为它区分了在同一个类里面不同的表达式类型.

让我们从一个最简单的表达式树开始, 将两个整数相加. 以下的表达式树表示2+3:

   1: Expression firstArg = Expression.Constant(2);
   2: Expression secondArg = Expression.Constant(3);
   3: Expression add = Expression.Add(firstArg, secondArg);
   4: Console.WriteLine(add);

运行上面的代码会产生一个”(2+3)”的输出, 另外还演示了不同的Expression类型还重载了ToString产生更加直观和利于人类阅读的输出. 值得一提的是在代码中的叶节点将会第一个被创建: 你是从底部往上创建表达式. 这主要是因为表达式是不可变的——只要你创建了一个表达式, 就将永远无法改变它. 因此你可以将其缓存然后重复使用.

现在我们已经构建了一个表达式树, 让我们来尝试来真正的执行它.

将表达式树编译成委托

从Expression继承的其中一个类就是LambdaExpression. 然后泛型Expression<TDelegate>类又继承了LambdaExpression, 这看起来容易让人混淆. Expression和Expression<TDelegate>的不同之处在于Expression<TDelegate>依照参数和返回类型严格的指出了表达式是那种类型. 显而易见, 这是由TDelegate参数类型指定的, 其必须是一个委托类型. 例如, 我们的简单加法表达式是一个没有参数并且返回一个int类型的表达式——这与Func<int>匹配, 因为我们可以使用Expression<Func<int>>来以一个静态类型的方式表示该表达式. 我们可以通过使用Expression.Lambda方法来完成这个工作. 该方法拥有大量的重载——我们例子使用了泛型方法, 该方法使用了一个类型参数指出我们想展示的委托类型.

那么, 这样做的目的是什么呢? LambdaExpression有一个Compile方法, 可以使用它来创建一个适当类型的委托. 现在该委托可以在普通的方式下被执行, 就像是它是用一个普通的方法或者其他方式来创建的一样. 以下的代码展示了这种做法:

   1: Expression firstArg = Expression.Constant(2);
   2: Expression secondArg = Expression.Constant(3);
   3: Expression add = Expression.Add(firstArg, secondArg);
   4: Func<int> compiled = Expression.Lambda<Func<int>>(add).Compile();
   5: Console.WriteLine(compiled());

上述的代码可能是最费解的用于打印出”5″的方式. 不过同时, 它也是相当令人印象深刻的. 我们以编程方式创建了一些逻辑块并将它们展现为普通对象, 然后让框架把所有的东西编译成”真实的”代码并且执行它. 你可能永远不需要像这样使用表达式树, 或者完全使用编程方式的来创建它们, 但这对于你了解LINQ是如何工作是非常有用的背景知识. 与CodeDOM不同的是, 表达式树仅仅只能用于展示单一表达式, 它们并不是被设计用于整个类, 方法甚至是语句(statements), 而且, C#通过lambda表达式直接在语言级别支持表达式树.

表达式树

“代码就是数据”是一个很古老的观念, 其并没有在很多流行的编程语言中使用. 你可以争论说所有的.NET程序都使用了这个观点, 因为JIT将所有IL代码都认为是数据, 并将他们转换成基于本机CPU的本地代码. 这隐藏得很深, 而且由于有存在的库用于操作IL代码, 因此它们并没有被广泛的应用.

.NET 3.5当中的提供了一种抽象的方式来将代码展现为一颗对象树. 这有点类似CodeDOM, 不过是在更高一层的级别上操作, 而且仅限于表达式. 表达式树的主要用处是在LINQ当中. C# 3当中对于将lambda表达式转换为表达式树提供了内建支持. 不过在我们开始讲解它们之前先来了解一下在没有编译器的帮助之下, 它们是如何适用于.NET Framework当中的.

编程方式构建表达式树

表达式树并没有听起来的那么神秘, 虽然有些时候他们的用法看起来的确有点像是魔术. 像它们的名字所展现的, 它们是对象树, 在树中的每一个节点都是其内部的一个表达式. 不同类型的表达式代表了可以在代码中执行的不同操作: 二进制操作, 例如加法; 一元操作, 例如读取数组长度; 方法调用; 构造器调用等等.

System.Linq.Expression命名空间包含了代表表达式的几个不同类. 它们全部都继承自Expression类, Expression是一个抽象的, 并且大部分由创建其他表达式实例的静态工厂方法组成. Expression类还暴露了两个属性:

  • Type属性代表了将要评估的表达式对应的.NET类型——你可以认为它是一个返回类型. 例如如果一个表达式读取了一个string的Length属性, 那么表达式的Type将为int.
  • NodeType属性返回表达式所代表的类型, 作为一个ExpressionType枚举的成员, 它可以是LessThan, Multiply和Invoke等等. 使用相同的例子, myString.Length当中, 属性访问部分将会得到一个MemberAccess的NodeType

有很多的类型都继承了Expression,其中的一部分有很多不同的节点类型(node types): 例如BinaryExpression, 代表了两个操作数之间的任何操作: 算术, 逻辑, 比较, 数组索引以及其他类似的操作. 这就是为什么NodeType在这里这么重要的原因, 因为它区分了在同一个类里面不同的表达式类型.

让我们从一个最简单的表达式树开始, 将两个整数相加. 以下的表达式树表示2+3:

   1: Expression firstArg = Expression.Constant(2);
   2: Expression secondArg = Expression.Constant(3);
   3: Expression add = Expression.Add(firstArg, secondArg);
   4: Console.WriteLine(add);

运行上面的代码会产生一个”(2+3)”的输出, 另外还演示了不同的Expression类型还重载了ToString产生更加直观和利于人类阅读的输出. 值得一提的是在代码中的叶节点将会第一个被创建: 你是从底部往上创建表达式. 这主要是因为表达式是不可变的——只要你创建了一个表达式, 就将永远无法改变它. 因此你可以将其缓存然后重复使用.

现在我们已经构建了一个表达式树, 让我们来尝试来真正的执行它.

将表达式树编译成委托

从Expression继承的其中一个类就是LambdaExpression. 然后泛型Expression<TDelegate>类又继承了LambdaExpression, 这看起来容易让人混淆. Expression和Expression<TDelegate>的不同之处在于Expression<TDelegate>依照参数和返回类型严格的指出了表达式是那种类型. 显而易见, 这是由TDelegate参数类型指定的, 其必须是一个委托类型. 例如, 我们的简单加法表达式是一个没有参数并且返回一个int类型的表达式——这与Func<int>匹配, 因为我们可以使用Expression<Func<int>>来以一个静态类型的方式表示该表达式. 我们可以通过使用Expression.Lambda方法来完成这个工作. 该方法拥有大量的重载——我们例子使用了泛型方法, 该方法使用了一个类型参数指出我们想展示的委托类型.

那么, 这样做的目的是什么呢? LambdaExpression有一个Compile方法, 可以使用它来创建一个适当类型的委托. 现在该委托可以在普通的方式下被执行, 就像是它是用一个普通的方法或者其他方式来创建的一样. 以下的代码展示了这种做法:

   1: Expression firstArg = Expression.Constant(2);
   2: Expression secondArg = Expression.Constant(3);
   3: Expression add = Expression.Add(firstArg, secondArg);
   4: Func<int> compiled = Expression.Lambda<Func<int>>(add).Compile();
   5: Console.WriteLine(compiled());

上述的代码可能是最费解的用于打印出”5″的方式. 不过同时, 它也是相当令人印象深刻的. 我们以编程方式创建了一些逻辑块并将它们展现为普通对象, 然后让框架把所有的东西编译成”真实的”代码并且执行它. 你可能永远不需要像这样使用表达式树, 或者完全使用编程方式的来创建它们, 但这对于你了解LINQ是如何工作是非常有用的背景知识. 与CodeDOM不同的是, 表达式树仅仅只能用于展示单一表达式, 它们并不是被设计用于整个类, 方法甚至是语句(statements), 而且, C#通过lambda表达式直接在语言级别支持表达式树.


« 
» 
快速导航

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3