zoukankan      html  css  js  c++  java
  • 深入学习C#匿名函数、委托、Lambda表达式、表达式树类型——Expression tree types

    匿名函数

      匿名函数(Anonymous Function)是表示“内联”方法定义的表达式。匿名函数本身及其内部没有值或者类型,但是可以转换为兼容的委托或者表达式树类型(了解详情)。匿名函数转换的计算取决于转换的目标类型:如果是委托类型,则转换计算为引用匿名函数所定义的方法的委托;如果是表达式树类型,则转换将计算以对象结构形式表示方法结构的表达式树。
      匿名函数有两种语法风格:Lambda表达式(lambda-expression)匿名方法表达式(anonymous-method-expression)。在几乎所有的情况下,Lambda表达式都比匿名方法表达式更为简介具有表现力。但现在C#语言中仍保留了后者,为了向后兼容。
      Lambda表达式:
        async可选 (匿名的函数签名)=> (匿名的函数体)
      匿名方法表达式:
        async可选 delegate (显式的匿名函数签名) 可选{代码块}

      其中匿名的函数签名可以包括两种,一种是隐式的匿名函数签名另一种是显式的匿名函数签名
        隐式的函数签名:(p)、(p1,p1)
        显式的函数签名:(int p)、(int p1,int p2)、(ref int p1,out int p2)
      匿名的函数体可以是表达式或者代码块。

      从上面我们可以看出,Lambda表达式的参数形式可以显式或者隐式类型化。在显式类型化参数列表中,每个参数的类型是显式声明的,在隐式类型化参数列表中,参数的类型是从匿名函数出现的上下文中推断出来的。
      当Lambda表达式只有一个具有隐式类型化参数的时候,参数列表可以省略圆括号,也就是说:
      (参数) => 表达式
      可以简写为
      参数 => 表达式

    一些匿名函数的示例

    x => x + 1 //隐式的类型化,函数体为表达式 
    x => {return x + 1;} //隐式的类型化,函数体为代码块 
    (int x) => x + 1 //显式的类型化,函数体为表达式 
    (int x) => {return x + 1;} //显式的类型化,函数体为代码块 
    (x , y) => x * y //多参数 
    () => Console.WriteLine() //无参数 
    async (t1 , t2) => await t1 + await t2 //异步 
    delegate (int x) {return x + 1;} //匿名函数方法表达式 
    delegate {return 1 + 1;} //参数列表省略   

    Lambda表达式和匿名方法表达式的区别:
      ● 当没有参数的时候,匿名方法表达式允许完全省略参数列表,从而可以转换为具有任意值参数列表的委托类型,Lambda表达式则不能省略参数列表的圆括号()。
      ● Lambda表达式允许省略和推断类型参数,而匿名方法表达式要求显式声明参数类型。
      ● Lambda表达式主体可以为表达式或者代码块,而匿名方法表达式的主体必须为代码块。
      ● 只有Lambda表达式可以兼容到表达式树类型。

    委托

      一个委托是一个指向一个方法的引用,或者说,一个委托的实例就是一个指向某个方法的对象,这是一个简单却十分强大的概念。
      C#中的委托是用来处理在其他语言中(如C++、Pascal等)需要用函数指针来处理的情况。不过与C++不同的是:委托是完全面向对象的;C++指针仅仅指向成员函数,而委托同时封装了对象的实例和方法;委托是完全类型安全的,只有当函数的签名与委托的签名匹配的时候,委托才可以指向该方法,当委托没有合法的指向方法的时候不能被调用。

      一些关于委托的知识点:
      
      1.委托是类型安全的
      委托类型的返回类型必须为void或者输出安全,委托类型的所有形参类型都必须是输入安全的。
      
      2.委托类型是名称等效,不是结构等效
      也就是说,对于两个委托类型,即使它们具有相同的参数列表和返回类型,它们仍将被视为两个不同的委托类型。
      例如:
      delegate int A(int x);
      delegate int B(int x);
      A和B是两个不同的委托。
      
      但是,两个结构一样的委托,它们的实例可以指向同一个方法。
      
      3.委托的调用列表(多播委托)
      委托实例所封装的方法集合称为调用列表。
      当我们从某个方法创建一个委托实例的时候,该实例将封装此方法,该实例中的调用列表包含一个“入口点”。当我们组合多个非空的委托实例的时候,它们的调用列表将连接在一起形成一个新的调用列表,新的调用列表中包含了多个“入口点”。
      委托的组合是使用二元运算符 + 和 += 来进行的,同样可以使用 - 和 -= 来进行组合的移除。
      
      下面的示例演示多个委托的实例化及其相应的调用列表:

    delegate void D(int x)
    class
    {
        public static void M1(int i){...}
        public static void M2(int i){...}
    }
    class Test
    {
        static void Main()
        {
            D cd1 = new D(c.M1);            //M1
            D cd2 = new D(c.M2);            //M2
            D cd3 = cd1 + cd2;              //M1 + M2
            D cd4 = cd3 + cd1;              //M1 + M2 + M1
            D cd5 = cd4 + cd3;              //M1 + M2 + M1 + M2
        }
    }

    实例化cd1和cd2的时候,它们分别封装一个方。实例化cd3的时候,它调用的列表有两个方法M1和M2,而且顺序与此相同。cd4的调用列表中依次包含M1、M2、M1。最后,cd5的调用列表中依次包含M2、M1、M1、M2。 

      4.委托的调用
      当调用一个委托实例的时候,将按照调用列表的顺序依次调用列表中的各个方法,当在调用期间发生异常,调用列表中排在后面的任何方法将不会被调用。

    using System;
    delegate void D(int x);
    class C
    {
        public static void M1(int i)
        {
            Console.WriteLine("C.M1:"+i);
        }
    
        public static void M2(int i)
        {
            Console.WriteLine("C.M1:"+i);
        }  
    
        public  void M3(int i)
        {
            Console.WriteLine("C.M2:"+i);
        }
    }
    
    class Test
    {
        static void Main()
        {
            D cd1 = new D(c.M1);            //M1
            cd1(-1);                        //调用cd1
    
            D cd2 = new D(c.M2);            //M2
            cd2(-2);                        //调用M2
    
            D cd3 = cd1 + cd2;              //M1 + M2
            cd3(10);                        //依次调用M1、M2
    
            cd3 += cd1;
            cd3(20);                        //依次调用M1、M2、M1
    
            C c = new C();
            D cd4 = new D(c.M3);            
            cd3 += cd4;
            cd3(30);                        //依次调用M1、M2、M1、M3
    
            cd3 -= cd1                      //移除最后一个M1
            cd3(40);                        //依次调用M1、M2、M3
    
            cd3 -= cd4;                     
            cd3(50);                        //依次调用M1、M2
    
            cd3 -= cd2
            cd3(60);                        //调用M1
    
            cd3 -= cd2                      //这是cd3的调用列表中没有cd2了,该移除不生效,不报错
            cd3(70);                        //调用M1
    
            cd3 -= cd1                      
            //      cd3(70);                //如果调用,将会报System.NullReferenceException异常
        }
    }

    Lambda表达式

      自从C#3.0开始,就可以使用一种新语法把实现代码赋予委托:Lambda表达式。只要有委托参数类型的地方,就可以使用Lambda表达式。使用匿名方法的地方可以使用Lambda表达式来代替,例如:

    Func< string,string > = delagate(string para)
    {
        para += "Hello World!";
        return param;
    }
    //可以写成
    Func< string,string > = para=>
    {
        para +="Hello World!";
        return para;
    }

      Lambda表达式运算符”=>”的左边列出了需要的参数,右边定义了赋予Lambda变量的方法的代码实现。 

      我们注意到,无论何时,只要我们需要引入匿名方法,我们都需要在前面加上delegate关键字,而且参数列表都需要类型化。Lambda表达式提供了一种更为简洁和自然的语法,而且在C#更多高级的方面中都涉及广泛。简而言之,我们应该用Lambda表达式来替代匿名方法。
      例如:

    public class Person
    {
        string Name;
        int Age;
    }
    
    List<Person> personList = new List<Person>();
    
    personList.Find(delegate(Person p){retrun p.Age==12;});
    //可以写成
    personList.Find(p=>p.Age==12);

     

    表达式树

    表达式树允许将 lambda 表达式表示为数据结构而非可执行代码。表达式目录树是System.Linq.Expressions.Expression< D > 形式的表达式目录树类型 (expression tree type) 的值,其中 D 是任何委托类型。 
      如果存在从 lambda 表达式到委托类型 D 的转换,则也存在到表达式树类型 Expression< D > 的转换。而lambda 表达式到委托类型的转换生成引用该 lambda 表达式的可执行代码的委托,到表达式树类型的转换创建该 lambda 表达式的表达式树表示形式。 
      表达式树是 lambda 表达式在内存中的高效数据表示形式,使 lambda 表达式的结构透明而又清晰。 
       
      与委托类型 D 一样, Expression< D > 具有与 D 相同的参数和返回类型。 
      下面的示例将 lambda 表达式表示为可执行代码和表达式树。因为存在到 Func< int,int > 的转换,所以也存在到 Expression< Func< int,int > > 的转换: 
      Func< int,int > del = x => x + 1; // Code 
      Expression< Func< int,int > > exp = x => x + 1; // Data 
      进行上面的赋值之后,委托 del 引用返回 x + 1 的方法,表达式目录树 exp 引用描述表达式 x => x +1 的数据结构。 
      泛型类型 Expression< D > 的确切定义以及将 lambda 表达式转换为表达式树类型时有关构造表达式树的准确规则不在本文的介绍范围之内,将另作说明。 
      有两个要点需要明确指出: 
      ● 并非所有 lambda 表达式都能转换为表达式树。例如,具有语句体的 lambda 表达式和包含赋值表达式的 lambda 表达式不能这样表示。在这些情况下,转换仍存在,但在编译时将失败 
      ● Expression< D > 提供一个实例方法 Compile,该方法产生一个类型为 D 的委托: 
    Func< int,int > del2 = exp.Compile(); 
      调用此委托将导致执行表达式树所表示的代码。因此,根据上面的定义, del 和 del2 等效,而且 
      下面的两个语句也将等效: 
      int i1 = del(1); 
      int i2 = del2(1); 
      执行此代码后, i1 和 i2 的值都为 2。

    c#中有Expression,即表达式。

    通过Expression可以动态构造代码,并编译执行。 比如:

    // 1.创建参数表达式 :
    ParameterExpression numParam = Expression.Parameter(typeof(int), "num");、
    //创建常量表达式:
    ConstantExpression five = Expression.Constant(5, typeof(int));
    //创建比较表达式:
    BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
    //创建标号:
    LabelTarget label = Expression.Label(typeof(int));
    //创建循环或者分支表达式:
      Expression.Loop(
        // Adding a conditional block into the loop.
               Expression.IfThenElse(
        // Condition: value > 1
                   Expression.GreaterThan(value, Expression.Constant(1)),
        // If true: result *= value --
                   Expression.MultiplyAssign(result,
                       Expression.PostDecrementAssign(value)),
        // If false, exit the loop and go to the label.
                   Expression.Break(label, result)
               ),
        // Label to jump to.
           label
        )
    //2.创建表达式树:
    Expression<Func<int, bool>> exprTree = num => num < 5;
    //3.分解表达式:
    ParameterExpression param = (ParameterExpression)exprTree.Parameters[0];
    //4.编译表达式:
    Func<int, bool> result = expr.Compile();

    表达式树的设计是基于"code as data"的思想,它把代码表示成树状的数据结构,树状结构中的每个节点都是一个表达式(这个表达式是一个广义的概念,并不是编程语言中所指的表达式语法),因此称为表达式树。

    表达式树的本质在于将代码组织在数据段,而不是代码段,这对于运行时更改代码是非常重要的。

    System.Linq.Expressions命名空间下含有很多类来表示不同的表达式,这些类都继承自抽象的Expression基类,Expression含有丰富的静态方法用于创建各种各样的表达式类。

    一、编程方式构建表达式树

    下面的代码以编程的方式构建表达式树

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

    上面的代码创建的表达式树如图:

     

    二、表达式树与代理(编译表达式树成为代理)

    将表达式树转化为代理的关键点在于Expression<TDelegate>类,继承关系如图: 

    可以使用Expression.Lambda方法创建Expression<TDelegate>对象,Expression<TDelegate>对象包含Compile方法,用于将表达式编译成可执行代码并生成表示其lambda表达式的代理对象,

    下面的代码表示转换过程: 

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

    三、表达式树与Lambda表达式(Lambda表达式转换为表达式树)

    可以通过lambda表达式构造Expression<TDelegate>对象:

    Expression<Func<string, string, bool>> expression =
    (x, y) => x.StartsWith(y);
    var compiled = expression.Compile();
    Console.WriteLine(compiled("First", "Second"));
    Console.WriteLine(compiled("First", "Fir"));

     下面的代码与之等价,但是用编程方式构建表达式树:

    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);
    var lambdaParameters = new[] { target, methodArg };
    var lambda = Expression.Lambda<Func<string, string, bool>>
    (call, lambdaParameters);
    var compiled = lambda.Compile();
    Console.WriteLine(compiled("First", "Second"));
    Console.WriteLine(compiled("First", "Fir"));

    目前,并不是所有的lambda表达式都能转换成表达式树,只有单一表达式的lambda表达式才可以转化为表达式树,而且表达式中不能包含赋值。

    四、表达式树与LINQ

    表达式树主要应用在LINQ to SQL中,LINQ to SQL的目标是将LINQ请求转换为SQL语句(普通文本),但是在转换过程中我们又不想失去编译时类型检查,所以LINQ to SQL的设计思想是将LINQ查询转化成表达式树(查询中使用的lambda表达式按照某种算法转换为表达式树),然后再将表达式树转换成需要执行的SQL语句。

  • 相关阅读:
    webpack 打包性能分析工具
    npm 使用
    npm 构建时,次要版本变化引起的问题
    AtomicStampedReference、AtomicMarkableReference 区别
    vue-cli 中的静态资源处理
    vue-cli 构建项目中 config/index.js 文件解读
    webpack的3个路径配置项: assetsRoot、assetsSubDirectory、assetsPublicPath
    Vue2 dist 目录下各个文件的区别
    DllPlugin、DllReferencePlugin 可以提取的第三方库列表
    JUC集合之 CopyOnWriteArrayList
  • 原文地址:https://www.cnblogs.com/Chary/p/9803921.html
Copyright © 2011-2022 走看看