zoukankan      html  css  js  c++  java
  • 深入理解委托、匿名方法和 Lambda 表达式

    前言

    lambda的概念建立在委托的基础上,委托,实现了类型安全的回调方法。在.NET 中回调无处不在,所以委托也无处不在,事件模型建立在委托机制上,Lambda 表达式本质上就是一种匿名委托。本文中将完成一次关于委托的旅行,全面阐述委托及其核心话题,逐一梳理委托、委托链、事件、匿名方法和 Lambda 表达式。

    委托的定义

    了解委托,从其定义开始,通常一个委托被声明为:

    public delegate void CalculateDelegate(Int32 x, Int32 y);

    关键字 delegate 用于声明一个委托类型 CalculateDelegate,可以对其添加访问修饰符,默认其返回值类型为 void,接受两个 Int32 型参数 x 和 y,但是委托并不等同与方法,而是一个引用类型,类似于 C++中的函数指针,稍后在委托本质里将对此有所交代。

    下面的示例将介绍如何通过委托来实现一个计算器模拟程序,在此基础上来了解关于委托的定义、创建和应用:

    using System;
    
    namespace DelegateAndEvent
    {
        public class DeepInto
        {
            //声明一个委托
            public delegate void CalculateDelegate(Int32 x, Int32 y);
    
            //创建与委托关联的方法,二者具有相同的返回值类型和参数列表
            public static void Add(Int32 x, Int32 y)
            {
                Console.WriteLine(x + y);
            }
    
            //定义委托类型变量
            private static CalculateDelegate myDelegate;
    
            public static void Run()
            {
                //进行委托绑定
                myDelegate = new CalculateDelegate(Add);
                //回调 Add 方法
                myDelegate(100, 200);
            }
        }
    }

    上述示例,在类 DelegateEx 内部声明了一个 CalculateDelegate 委托类型,它具有和关联方法Add 完全相同的返回值类型和参数列表,否则将导致编译时错误。将方法 Add 传递给 CalculateDelegate 构造器,也就是将方法 Add 指派给 CalculateDelegate 委托,并将该引用赋给 myDelegate 变量,也就表示 myDeleage 变量保存了指向 Add 方法的引用,以此实现对 Add 的回调。

    由此可见,委托表示了对其回调方法的签名,可以将方法当作参数进行传递,并根据传入的方法来动态的改变方法调用。只要为委托提供相同签名的方法,就可以与委托绑定,例如:

    public static void Subtract(Int32 x, Int32 y) {
        Console.WriteLine(x - y);
    }

    同样,可以将方法 Subtract 分配给委托,通过参数传递实现方法回调,例如:

    public static void Main()
    {
        //进行委托绑定
        myDelegate = new CalculateDelegate(Subtract);
        myDelegate(100, 200);
    }

    C# 中委托的演变

    在 C# 1.0 中,通过使用在代码中其他位置定义的方法显式初始化委托来创建委托的实例。 C# 2.0 引入了匿名方法的概念,作为一种编写可在委托调用中执行的未命名内联语句块的方式。 C# 3.0 引入了 Lambda 表达式,这种表达式与匿名方法的概念类似,但更具表现力并且更简练。 这两个功能统称为匿名函数。 通常,面向 .NET Framework 3.5 及更高版本的应用程序应使用 lambda 表达式。

    C#1.0中委托的实现,代码如下:

     delegate int CalculateHandler(int x, int y);
     private int Sum(int x, int y)
     {
        return x + y;
     }
     public void Test()
     {
        CalculateHandler sumHandler =new CalculateHandler(Sum);
        MessageBox.Show(sumHandler(1, 2).ToString());//输入结果3
    }

    C#2.0中匿名方法的实现,代码如下:

    delegate int CalculateHandler(int x, int y);
    public void Test()
    {
        CalculateHandler sumHandler = delegate(int x, int y) { return x + y; };   //这是一个匿名委托
        MessageBox.Show(sumHandler(1, 2).ToString());//输入结果3
    }

    C#3.0中Lambda 表达式的实现,代码如下:

    delegate int CalculateHandler(int x, int y);
     public void Test()
     {
          CalculateHandler sumHandler = (x, y) => x + y;     //Lambda 表达式本质上就是一种匿名委托 delegate(int x, int y) { return x + y; }; 之所以有Lambda是因为对这种匿名委托写法的升级,是一种c#的语法糖
     MessageBox.Show(sumHandler(1, 2).ToString());//输入结果3 }

    由此可以看出微软的一步步升级,带给我们的是编程上的优美,简洁,可读性强,因此作为程序员我们要一直处于学习的路上。

    问题:c#编程中什么时候用lambda表达式?

    回答:在用匿名委托时可以用lambda表达式,例如:db.student.where(p=>p.no=="001"); 因为where(T1)中的参数T1是一个委托类型,所以这里可以用p=>p.no=="001"匿名委托为其赋值。

    多播委托和委托链

    在上述委托实现中,Add 方法和 Subtract 可以绑定于同一个委托类型 myDelegate,由此可以很容易想到将多个方法绑定到一个委托变量,在调用一个方法时,可以依次执行其绑定的所有方法,这种技术称为多播委托。在.NET 中提供了相当简洁的语法来创建委托链,以+=和-=操作符分别进行绑定和解除绑定的操作,多个方法绑定到一个委托变量就形成一个委托链,对其调用时,将会依次调用所有绑定的回调方法。例如:

    myDelegate = new CalculateDelegate(Add);
    myDelegate += new CalculateDelegate(Subtract);
    myDelegate += new CalculateDelegate(Multiply);
    myDelegate(100, 200);

    上述执行将在控制台依次输出 300、-100 和 20000 三个结果,可见多播委托按照委托链顺序调用所有绑定的方法,同样以-=操作可以解除委托链上的绑定,例如:

    myDelegate -= new CalculateDelegate(Add);
    myDelegate(100, 200);

    结果将只有-100 和 20000 被输出,可见通过-=操作解除了 Add 方法。 事实上,+=和-=操作分别调用了 Deleagate.Combine 和 Deleagate.Remove 方法,由对应的 IL 可知:

    .method public hidebysig static void Main() cil managed
    {
    .entrypoint
    // 代码大小 151 (0x97)
    .maxstack 4
    IL_0000: nop
    IL_0001: ldnull
    IL_0002: ldftn void 
    InsideDotNet.NewFeature.CSharp3.DelegateEx::Add(int32, int32)
    //部分省略……
    IL_0023: call class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Combine(class [ms
    corlib]System.Delegate, class [mscorlib]System.Delegate)
    //部分省略……
    IL_0043: call class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Combine(class [ms
    corlib]System.Delegate, class [mscorlib]System.Delegate)
    //部分省略……
    IL_0075: call class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Remove(class [ms
    corlib]System.Delegate, class [mscorlib]System.Delegate)
    //部分省略……
    IL_0095: nop
    IL_0096: ret
    } // end of method DelegateEx::Main

    所以,上述操作实际等效于:

    public static void Main()
    {
        myDelegate = (CalculateDelegate)Delegate.Combine(new CalculateDelegate(Add),
            new CalculateDelegate(Subtract), new CalculateDelegate(Multiply));
        myDelegate(100, 200);
        myDelegate = (CalculateDelegate)Delegate.Remove(myDelegate, 
            new CalculateDelegate(Add));
        myDelegate(100, 200);
    }

    另外,多播委托返回值一般为 void,委托类型为非 void 类型时,多播委托将返回最后一个调 用的方法的执行结果,所以在实际的应用中不被推荐。

    委托的本质

    委托在本质上仍然是一个类,如此简洁的语法正是因为 CLR 和编译器在后台完成了一系列操作,将上述 CalculateDelegate 委托编译为 IL,你将会看得更加明白,如图:

    所以,委托本质上仍旧是一个类,该类继承自 System.MulticastDelegate 类,该类维护一个带有链接的委托列表,在调用多播委托时,将按照委托列表的委托顺序而调用的。还包括一个接受两个参数的构造函数和 3 个重要方法:BeginInvoke、EndInvoke 和 Invoke。

    首先来了解 CalculateDelegate 的构造函数,它包括了两个参数:第一个参数表示一个对象引用,它指向了当前委托调用回调函数的实例,在本例中即指向一个 DelegateEx 对象;第二个参数标识了回调方法,也就是 Add 方法因此,在创建一个委托类型实例时,将会为其初始化一个指向对象的引用和一个标识回调方法的整数,这是由编译器完成的。那么一个回调方法是如何被执行的,继续以 IL 代码来分析委托的调用,即可显露端倪(在此仅分析委托关联 Add 方法时的情况):

    .method public hidebysig static void Main() cil managed
    {
    .entrypoint
    // 代码大小 37 (0x25)
    .maxstack 8
    IL_0000: nop
    IL_0001: ldnull
    IL_0002: ldftn void 
    InsideDotNet.NewFeature.CSharp3.DelegateEx::Add(int32, int32)
    IL_0008: newobj instance void InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelega
    te::.ctor(object, native int)
    IL_000d: stsfld class InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegate InsideD
    otNet.NewFeature.CSharp3.DelegateEx::myDelegate
    IL_0012: ldsfld class 
    InsideDotNet.NewFeature.CSharp3.DelegateEx/Calculate Delegate InsideDotNet.NewFeature.CSharp
    3.DelegateEx::myDelegate
    IL_0017: ldc.i4.s 100
    IL_0019: ldc.i4 0xc8
    IL_001e: callvirt instance void InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegat
    e::Invoke(int32, int32)
    IL_0023: nop
    IL_0024: ret} // end of method DelegateEx::Main

    在 IL 代码中可见,首先调用 CalculateDelegate 的构造函数来创建一个 myDelegate 实例,然后通过 CalculateDelegate::Invoke 执行回调方法调用,可见真正执行调用的是 Invoke 方法。因此,你也可以通过 Invoke 在代码中显示调用,例如:

    myDelegate.Invoke(100, 200

    其执行过程和隐式调用是一样的,注意在.NET 1.0 中 C#编译器是不允许显示调用的,以后的版本中修正了这一限制。

    另外,Invoke 方法直接对当前线程调用回调方法,在异步编程环境中,除了 Invoke 方法,也会生成 BeginInvoke 和 EndInvoke 方法来完成一定的工作。这也就是委托类中另外两个方法的作用。

    委托和事件

    .NET 的事件模型建立在委托机制之上,透彻的了解了委托才能明白的分析事件。可以说,事件是对委托的封装,从委托的示例中可知,在客户端可以随意对委托进行操作,一定程度上破坏了面向的对象的封装机制,因此事件实现了对委托的封装。 下面,通过将委托的示例进行改造,来完成一个事件的定义过程:

    using System;
    
    namespace DelegateAndEvent
    {
        public class Calculator
        {
            //定义一个 CalculateEventArgs,
            //用于存放事件引发时向处理程序传递的状态信息
            public class CalculateEventArgs : EventArgs
            {
                public readonly Int32 x, y;
    
                public CalculateEventArgs(Int32 x, Int32 y)
                {
                    this.x = x;
                    this.y = y;
                }
            }
    
            //声明事件委托
            public delegate void CalculateEventHandler(object sender, CalculateEventArgs e);
    
            //定义事件成员,提供外部绑定
            public event CalculateEventHandler MyCalculate;
    
            //提供受保护的虚方法,可以由子类覆写来拒绝监视
            protected virtual void OnCalculate(CalculateEventArgs e)
            {
                if (MyCalculate != null)
                {
                    MyCalculate(this, e);
                }
            }
    
            //进行计算,调用该方法表示有新的计算发生
            public void Calculate(Int32 x, Int32 y)
            {
                CalculateEventArgs e = new CalculateEventArgs(x, y);
                //通知所有的事件的注册者
                OnCalculate(e);
            }
        }
    }

    示例中,对计算器模拟程序做了简要的修改,从二者的对比中可以体会事件的完整定义过程,主要包括:

    • 定义一个内部事件参数类型,用于存放事件引发时向事件处理程序传递的状态信息,EventArgs是事件数据类的基类。
    • 声明事件委托,主要包括两个参数:一个表示事件发送者对象,一个表示事件参数类对象。
    • 定义事件成员。
    • 定义负责通知事件引发的方法,它被实现为 protected virtual 方法,目的是可以在派生类中覆写该方法来拒绝监视事件。
    • 定义一个触发事件的方法,例如 Calculate 被调用时,表示有新的计算发生。

    一个事件的完整程序就这样定义好了。然后,还需要定义一个事件触发程序,用来监听事件:

    //定义事件触发者
    public class CalculatorManager
    {
        //定义消息通知方法
        public void Add(object sender, Calculator.CalculateEventArgs e)
        {
            Console.WriteLine(e.x + e.y);
        }
    
        public void Substract(object sender, Calculator.CalculateEventArgs e)
        {
            Console.WriteLine(e.x - e.y);
        }
    }

    最后,实现一个事件的处理程序:

    public class Test_Calculator
    {
        public static void Run()
        {
            Calculator calculator = new Calculator();
            //事件触发者
            CalculatorManager cm = new CalculatorManager();
            //事件绑定
            calculator.MyCalculate += cm.Add;
            calculator.Calculate(100, 200);
            calculator.MyCalculate += cm.Substract;
            calculator.Calculate(100, 200);
            //事件注销
            calculator.MyCalculate -= cm.Add;
            calculator.Calculate(100, 200);
        }
    }

    如果对设计模式有所了解,上述实现过程实质是 Observer 模式在委托中的应用,在.NET 中对Observer 模式的应用严格的遵守了相关的规范。在 Windows Form 程序开发中,对一个Button 的Click 就对应了事件的响应,例如:

    this.button1.Click += new System.EventHandler(this.button1_Clic

    用于将 button1_Click 方法绑定到 button1 的 Click 事件上,当有按钮被按下时,将会触发执行 button1_Click 方法:

    private void button1_Click(object sender, EventArgs e) { }

    匿名方法

    匿名方法以内联方式放入委托对象的使用位置,而避免创建一个委托来关联回调方法,也就是由委托调用了匿名的方法,将方法代码和委托实例直接关联,在语法上有简洁和直观的好处。例如以匿名方法来绑定 Click 事件将变得非常简单:

    button1.Click += delegate
    {
        MessageBox.Show("Hello world.");
    };

    因此,有必要以匿名方法来实现本节开始的委托示例,了解其实现过程和底层实质,例如:

    class AnonymousMethodEx
    {
        delegate void CalculateDelegate(Int32 x, Int32 y);
    
        public static void Run()
        {
            //匿名方法
            CalculateDelegate mySubstractDelegate = delegate(Int32 x, Int32 y) { Console.WriteLine(x - y); };
            CalculateDelegate myAddDelegate = delegate(Int32 x, Int32 y) { Console.WriteLine(x + y); };
            mySubstractDelegate(100, 200);
        }
    }

    事实上,匿名方法和委托在 IL 层是等效的,编译器为匿名方法增加了两个静态成员和静态方法,如图:

    由编译器生成的两个静态成员和静态方法,辅助实现了委托调用一样的语法结构,这正是匿名方法在底层的真相。

    Lambda表达式

    Lambda 表达式是 Functional Programming 的核心概念,现在 C# 3.0 中也引入了 Lambda 表达式来实现更加简洁的语法,并且为 LINQ 提供了语法基础,这些将在本书第 12 章有所交代。再次应用 Lambda 表达式来实现相同的过程,其代码为:

    using System;
    
    namespace Lambda
    {
        public class LambdaExpressionEx
        {
            delegate void CalculateDelegate(Int32 x, Int32 y);
    
            public static void Run()
            {
                CalculateDelegate myDelegate = (x, y) => Console.WriteLine(x - y);
                myDelegate(100, 200);
            }
        }
    }

    分析 Lambda 表达式的 IL 代码,可知编译器同样自动生成了相应的静态成员和静态方法,Lambda 表达式在本质上仍然是一个委托。带来这一切便利的是编译器,在此对 IL 上的细节不再做进一步分析。

    规则

    • 委托实现了面向对象的,类型安全的方法回调机制。
    • 以 Delegate 作为委托类型的后缀,以 EventHandle 作为事件委托的后缀,是规范的命名规则。
    • 多播委托返回值一般为 void,不推荐在多播委托中返回非 void 的类型。
    • 匿名方法和 Lambda 表达式提供了更为简洁的语法表现,而这些新的特性主要是基于编译器而实现的,在 IL 上并没有本质的变化。
    • .NET 的事件是 Observer 模式在委托中的应用,并且基于.NET 规范而实现,体现了更好的耦合性和灵活性。

    结论

    从委托到 Lambda 表达式的逐层演化,我们可以看到.NET 在语言上的不断进化和发展,也正是这些进步促成了技术的向前发展,使得.NET 在语言上更加地兼容和优化。对于技术开发人员而言,这种进步也正是我们所期望的。 然而,从根本上了解委托、认识委托才是一切的基础,否则语法上的进化只能使得理解更加迷惑。本节的讨论,意在为理解这些内容提供基础,建立一个较为全面的概念。

  • 相关阅读:
    北京清北 综合强化班 Day1
    Noip2015 提高组 Day1
    Noip2016 提高组 Day2 T1 组合数问题
    2017.9.23 NOIP2017 金秋杯系列模拟赛 day1 T1
    [51NOD1103] N的倍数(鸽笼原理)
    [51NOD1420] 数袋鼠好有趣(贪心)
    [CF808A] Lucky Year(规律)
    [CF808B] Average Sleep Time([强行]树状数组,数学)
    [CF808C] Tea Party(贪心)
    [CF808D] Array Division(暴力,枚举)
  • 原文地址:https://www.cnblogs.com/happyShare/p/12319185.html
Copyright © 2011-2022 走看看