首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

8.5  一脉相承:委托、匿名方法和Lambda表达式

本节将介绍以下内容:

— 委托

— 事件

— 匿名方法

—  Lambda表达式

8.5.1  引言

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

8.5.2  解密委托

1.委托的定义

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

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

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

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

class DelegateEx

{

    //声明一个委托

    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 Main()

    {

        //进行委托绑定

        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);

}

2.多播委托和委托链

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

public static void Main()

{

    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 [mscorlib]System.Delegate, class [mscorlib]System.Delegate)

  //部分省略……

  IL_0043:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)

  //部分省略……

  IL_0075:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Remove(class [mscorlib]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类型时,多播委托将返回最后一个调用的方法的执行结果,所以在实际的应用中不被推荐。

3.委托的本质

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

图8-2  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/ CalculateDelegate::.ctor(object, native int)

  IL_000d:  stsfld     class InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegate InsideDotNet.NewFeature.CSharp3.DelegateEx::myDelegate

  IL_0012:  ldsfld     class

InsideDotNet.NewFeature.CSharp3.DelegateEx/Calculate Delegate InsideDotNet.NewFeature.CSharp3.DelegateEx::myDelegate

  IL_0017:  ldc.i4.s   100

  IL_0019:  ldc.i4     0xc8

  IL_001e:  callvirt   instance void InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegate::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方法来完成一定的工作。这也就是委托类中另外两个方法的作用。

8.5.3  委托和事件

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

下面,通过将委托的示例进行改造,来完成一个事件的定义过程:

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);

    }

}

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

l  定义一个内部事件参数类型,用于存放事件引发时向事件处理程序传递的状态信息,EventArgs是事件数据类的基类。

l  声明事件委托,主要包括两个参数:一个表示事件发送者对象,一个表示事件参数类对象。

l  定义事件成员。

l  定义负责通知事件引发的方法,它被实现为protected virtual方法,目的是可以在派生类中覆写该方法来拒绝监视事件。

l  定义一个触发事件的方法,例如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 Main()

    {

        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_Click);

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

private void button1_Click(object sender, EventArgs e)

{

}

8.5.4  匿名方法

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

button1.Click += delegate

{

    MessageBox.Show("Hello world.");

};

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

class AnonymousMethodEx

{

    delegate void CalculateDelegate(Int32 x, Int32 y);

    public static void Main()

    {

        //匿名方法

        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层是等效的,编译器为匿名方法增加了两个静态成员和静态方法,如图8-3所示。

图8-3  匿名方法的IL分析

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

8.5.5  Lambda表达式

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

class LambdaExpressionEx

{

    delegate void CalculateDelegate(Int32 x, Int32 y);

    public static void Main()

    {

        CalculateDelegate myDelegate = (x, y) => Console.WriteLine(x - y);

        myDelegate(100, 200);

    }

}

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

8.5.6  规则

l  委托实现了面向对象的,类型安全的方法回调机制。

l  以Delegate作为委托类型的后缀,以EventHandle作为事件委托的后缀,是规范的命名规则。

l  多播委托返回值一般为void,不推荐在多播委托中返回非void的类型。

l  匿名方法和Lambda表达式提供了更为简洁的语法表现,而这些新的特性主要是基于编译器而实现的,在IL上并没有本质的变化。

l  .NET的事件是Observer模式在委托中的应用,并且基于.NET规范而实现,体现了更好的耦合性和灵活性。

8.5.7  结论

从委托到Lambda表达式的逐层演化,我们可以看到.NET在语言上的不断进化和发展,也正是这些进步促成了技术的向前发展,使得.NET在语言上更加地兼容和优化。对于技术开发人员而言,这种进步也正是我们所期望的。

然而,从根本上了解委托、认识委托才是一切的基础,否则语法上的进化只能使得理解更加迷惑。本节的讨论,意在为理解这些内容提供基础,建立一个较为全面的概念。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论