C#复习笔记(4)--C#3:革新写代码的方式(Lambda表达式和表达式树)

2021-04-19 11:26

阅读:473

Lambda表达式和表达式树

先放一张委托转换的进化图

技术分享图片

看一看到lambda简化了委托的使用。

lambda可以隐式的转换成委托或者表达式树。转换成委托的话如下面的代码:

Funcstring, int> getLength = s => s.Length;

转换成表达式树的话是下面的代码:

 Expressionstring, int>> getLength = s => s.Length;

委托方面的东西前面都做了详细的介绍。我们主要学习表达式树

表达式树

表达式是当今编程语言中最重要的组成成分。简单的说,表达式就是变量、数值、运算符、函数组合起来,表示一定意义的式子。例如下面这些都是(C#)的表达式:

3 //常数表达式
a //变量或参数表达式
!a //一元逻辑非表达式
a + b //二元加法表达式
Math.Sin(a) //方法调用表达式
new StringBuilder() //new 表达式
myString.length//MemberAccess表达式

首先澄清一个概念:表达式。表达式是一个以;结尾的句子,如果是两句,那就不叫表达式了,比如下面不是表达式:

{
....
}

代码作为数据是一个古老的概念,.NET3. 5的表达式树提供了一种抽象的方式将一些代码表示成一个对象树。 它类似于CodeDOM, 但是在一个稍高的级别上操作。 表达式树主要用于LINQ, 本节稍后会解释表达式树对于整个LINQ的重要性。

System.Linq.Expressions命名空间包含了代表表达式的各个类, 它们都继承自Expression,一个抽象的主要包含一些静态工厂方法的类, 这些方法用于创建其他表达式类的实例。 然而,Expression 类也包括两个属性。

  • Type属性代表表达式求值后的.NET类型, 可把它 视为一个返回类型。 例如,如果一个表达式要获取一个字符串的Length属性, 该表达式的类型就是int。
  • NodeType属性返回所代表的表达式的种类。 它是ExpressionType枚举的成员, 包括LessThan、Multiply和Invoke等。 仍然使用上面的例子,对于myString. Length 这个属性访问来说, 其节点类型是MemberAccess。
 static void Main(string[] args)
        {
            Expression firstArg = Expression.Constant(2);
            Expression secondArg = Expression.Constant(3);
            BinaryExpression add = Expression.Add(firstArg, secondArg);
            Console.WriteLine(add);           
            Console.ReadKey();
        }

技术分享图片

上面的结果最终会输出(2+3).这意味着这些表达式树类覆盖了ToString来产生可读的输出。

就像代码中所做的,首先创建的是叶表达式,然后自下而上的去创建这个完整的表达式。这是由“ 表达式不易变” 这一事实决定的——创建好表达式后, 它就永远不会改变。 这样就可以随心所欲地缓存和重用表达式。

LambdaExpression是从Expression派生的类型之一。 泛型类Expression又是从LambdaExpression派生的。

技术分享图片

Expression和Expression 类的区别在于, 泛型类以静态类型的方式标识了它是什么种类的表达式,也就是说,它确定了返回类型和参数。 很明显, 这是用TDelegate类型参数来表示的, 它必须是一个委托类型。 例如, 假设我们的简单加法表达式就是一个 不获取任何参数, 并返回整数的委托。 与之匹配的签名就是Func, 所以可以使用一个Expression >, 以静态类型的方式表示该表达式。 我们用Expression.Lambda 方法来完成这件事。 该方法有许多重载版本——我们的例子使用的是泛型方法, 它用一个类型参数来指定我们想要表示的委托的类型。

 static void Main(string[] args)
        {

            Expression firstArg = Expression.Constant(2);
            Expression secondArg = Expression.Constant(3);
            BinaryExpression add = Expression.Add(firstArg, secondArg);
            Funcint> lambda = Expression.Lambdaint>>(add).Compile();
            Console.WriteLine(lambda());
            Console.ReadKey();
        }

Expression.Lambda有泛型的重载,指示表达式可以转换为一个类型实参为Func的Expression,然后,可以通过Compile方法来将表达式树编译成委托。

印象。我们在程序中创建了一些逻辑块(比如Expression firstArg = Expression.Constant(2);), 将其表示成普通对象, 然后要求框架将所有的东西都编译成可以执行的“真实” 的代码。 你或许永远都不需要真正以这种方式使用表达式树, 甚至永远都不需要在程序中构造 它们,但它提供了相当有用的背景知识,可以帮助你理解LINQ是怎样工作的。

上面介绍的是将表达式树编译成lambda,下面介绍的是----

将lambda转换成表达式树

我们知道,Lambda表达式能显式或隐式地转换成恰当的委托实例。 然而, 这并非唯一能进行的转换。 还可以要求编译器通过你的Lambda 表达式构建一个表达式树, 在执行时创建Expression 的一个实例。 例如,下面展示了用一种精简得多的方式 创建“返回 5” 的表达式, 然后编译这个表达式, 并调用编译得到的委托。

 static void Main(string[] args)
        {

            Expressionint>> return5 = () => 5;
            Funcint> lambda = return5.Compile();
            Console.WriteLine(lambda());
            Console.ReadKey();
        }

一些限制:

  • 并非**所有** Lambda表达式都能转换成表达式树。 不能将带有一个语句块(即使只有一个return语句) 的Lambda 转换成表达式树—— 只有对单个表达式进行求值的Lambda才可以。
  • 表达式中还不能包含赋值操作,因为在表达式树中表示不了这种操作。 尽管.NET4 扩展了表达式树的功能, 但只能转换单一表达式这一限制仍然有效。 
  • 还有一些其他的限制,不过很少见,你会在编译错误的时候得到编译器的提示

更复杂的例子:

 static void Main(string[] args)
        {

            Expressionstring, string, bool>> expression = (x, y) => x.StartsWith(y);
            var lambda = expression.Compile();
            Console.WriteLine(lambda("first that i did","first"));//true
            Console.ReadKey();
        }

上面这个例子如果用表达式树来做的话是非常复杂的:

 static void Main(string[] args)
        {
           
            MethodInfo method = typeof(string).GetMethod("StartsWith", new []{typeof(string)});//①构造这个方法调用的各个部件
            var target = Expression.Parameter(typeof(string), "x");
            var methodArg = Expression.Parameter(typeof(string), "y");
            Expression[] methodArgs =new[] {methodArg};
            Expression call = Expression.Call(target, method, methodArgs);//②从以上部件构建callexpression
            var lambdaParameters = new[]{target, methodArg};//③将callexpression转化成lambda
            var lambda = Expression.Lambdastring,string,bool>>(call,lambdaParameters);
            var compiled = lambda.Compile();
            Console.WriteLine(compiled("first","second"));//false
            Console.WriteLine(compiled("first", "fir"));//true
            Console.ReadKey();
        }

首先感谢编译器能够让lambda隐式转化成表达式树!

唯一的好处是它确实更清晰地展示树中涉及的东西以及参数是如何绑定的。 为了构造最终的方法调用表达式, 我们需要知道方法调用的几个部件①, 其中包括: 方法的目标(也就是调用StartsWith的字符串);方法本身( MethodInfo);参数列表(本例只有一个参数)。在本例中, 方法的目标和参数恰好都是传递给表达式的参数, 但它们完全可能是其他表达式类型, 如常量、其他方法调用的结果、属性的求值结果, 等等。 将方法调用构造成一个表达式之后 ?, 接着需要把它转换成Lambda表达式 ?, 并绑定参数。 我们重用 了作为方法调用(部件)信息而创建的参数表达式的值(ParameterExpression): 创建Lambda表达式时指定的参数顺序就是最终调用委托时使用的参数顺序。

技术分享图片

这是编译好之后的表达式树。

位于linq核心的表达式树

没有lambda表达式,表达式树几乎没有任何价值。从一定程度上说,没有表达式树,lambda也就没那么有用了。LINQ在C#的全部体现就包括lambda、表达式树和扩展方法这三部分。

长期以来, 我们要么能在编译时进行很好的检查, 要么能指示另一个平台运行一些代码, 这些指示一般表示成文本(如SQL查询)。 但是,鱼和熊掌不可兼得(这个却是lambda的优点,既能转化成委托,在进程内配合迭代器进行处理集合序列,又能编译成表达式树,以供其他提供器翻译成另一个平台上的语言,比如sql)。 Lambda表达式提供 了编译时检查的能力, 而表达式树可以将执行模型从你所需的逻辑中提取出来。 将两者合并到一起之后, 鱼和熊掌就能兼得 了—— 当然是在一个合理的范畴之内。“ 进程外” LINQ提供器的中心思想在于, 我们可以从一个熟悉的源语言(如 C#) 生成一个表达式树, 将结果作为一个中间格式, 再将其转换成目标平台上的本地语言, 比如SQL。 某些时候, 你更多地会遇到一个本机API, 而不是一种简单的本机语言。 例如, 这个API可能根据表达式所表示的 内容来调用不同的Web服务。下图展示LINQ to Objects 和 LINQ to SQL 的 不同 路径。

技术分享图片

 

 除了LINQ,表达式树也可以用在别的地方

1、我们在以后的内容中讨论C#动态类型 时, 将看到更多关于动态语言运行时的内容。 表达式树是其架构的核心部分。 它们具有三个特点对DLR特别有吸引力:

  • 它们是不易变的, 因此可以安全地缓存;
  • 它们是可组合的, 因此可以在简单的块中构建出复杂的行为;
  • 它们 可以编译为委托, 后者可以像平常那样进一步JIT 编译为本地代码。

2、可以放心地对成员的引用进行重构

以后C#会推出一个infoof的操作符,但具体是干啥的还不知道,这里先做标记。以后来补充。

3、其他。。。。

这个回头再来看一下书上的介绍吧。貌似没有用到过

类型推断和重载决策的改变

C#3中lambda表达式的加入使得原先的类型推断和重载决策为了新的环境而做了改变。


评论


亲,登录后才可以留言!