首页 » .NET » 委托与事件

委托与事件

原文 http://blog.csdn.net/judyyduj/article/details/78797972

2018-01-27 02:00:25阅读(193)

本篇内容是阅读《深入理解C#》和《C#与.net4高级程序设计》的笔记,关注于委托这一部分。

1. 委托是用来干什么的

委托相当于一个对方法(method)的接口,它提出有这样一种方法:已经知晓了它们的名称和类型,即已经声明了,但是这个方法要怎样具体实施以达成目的还不清楚,需要在另外的地方给出具体的代码。
委托是对一类方法的抽象,要执行具体的工作需要调用具体的委托实例。就像类(class)是对一类事物的抽象,真正在实际中使用的是具体的对象(object)。
委托的好处是提供了一个更工程化的思路:当我知道需要这样一个方法时,我只考虑它的效果,而把具体应该怎样做这个问题留给真正执行这些操作的人来考虑。一方面,我不知道具体执行时候的情况是怎样的,给出细节的代码很困难;另一方面,把事情的结果和过程分隔开也提高了考虑事情的效率。
本质上,委托是一个类型安全的对象,它指向程序中另一个(或多个)以后会被调用的方法。

2. 使用委托的4个步骤 a. 声明委托类型(delegate type)

规定参数类型列表和返回类型,如:

    delegate void StringProcessor(string input);
b. 要执行的代码,必须有某个方法告知了这个委托到底要做什么

首先为委托实例执行时使用的方法(代码)起一个名字,叫“委托实例的操作”(the action of the delegate instance)。这里所讨论等东西没有官方叫法,这个名字是《C# in Depth》作者Jon Skeet自己起的,中文参考中文版的翻译。
可能提供“委托实例的操作”的方法满足下面两个特点(称为有同样的签名(signature)):
- 参数列表和委托类型所声明的相同,即有相同的数量,且对应参数是同一个类型。
- 返回值的类型相同。
比如(针对前面的例子)

    void PrintString(string x)
c. 创建委托实例(delegate instance) 如果委托实例和“委托实例的操作”在一个类(class)中
    StringProcessor proc = new StringProcessor(PrintString);
不在一个类中时,需要限定“委托实例等操作”是定义在哪个类里的
i. 如果操作是静态的,比如PrintString是StatisticMethods类中的一个方法:
    StringProcessor proc1 = new StringProcessor(StatisticMethod.PrintString);
   ii. 如果操作是实例方法,比如PrintString是InstanceMethods类中的一个实例方法:
InstanceMethods instance = new InstanceMethods;
StringProcessor proc2 = new StringProcessor(instance.PrintString);
d. 调用(invoke)委托实例

delegate类中提供了用于调用委托实例的方法:Invoke。这个方法和所声明的委托类型使用相同的参数列表和返回类型。
在C#中,delegate类的表达式(通常就是这个变量或实例)可以为视为一个方法,也就是说proc1(“Hello”)和proc1.Invoke(“Hello”)的效果是一样的,实际上,前者会被编译成后者。

3. 一个简单的例子
   using System;
   delegate void StringProcessor(string input); // 声明委托类型
   class Person
   {
       string name;
       public Person(string name) { this.name = name; }
       public void Say(string message) // 委托实例的操作
       {
           Console.WriteLine("{0} says: {1}", name, message);
       }
   }
   class Background
   {
       public static void Note(string note) // 委托实例的操作
       {
           Console.WriteLine("({0})", note);
       }
   }
   class SimpleDelegateUse
   {
       static void Main()
       {
           Person jon = new Person("Jon");
           Person tom = new Person("Tom");
           // 创建委托实例
           StringProcessor jonsVoice, tomsVoice, background; 
           jonsVoice = new StringProcessor(jon.say);
           tomsVoice = new StringProcessor(tom.say);
           background = new StringProcessor(Background.Note);
           // 调用委托实例
           jonsVoice("Hello, son.");
           tomsVoice("Hello, Daddy!");
           background("An airplane flies past.");
       }
   }
4. 当我们使用委托时,程序究竟做了什么

.net中的System.Delegate和System.MulticastDelegate构建了委托这个概念。使用Delegate关键字创建委托时,间接(自动地)声明了一个派生自System.MulticastDelegate的类。(System.MulticastDelegate是System.Delegate的子类。)
比如,关于委托的调用,定义委托类型时会生成一个密封类,其中有3个编译器生成的方法,这个3个方法的参数和返回值基于委托的声明。下面的伪码说明了这个模式:

     public sealed class DelegateName : System.MulticastDelegate
       {
           public delegateReturnValue Invoke(allDelegateInputRefAndOutParams);
           public IAsyncResult BeginInvoke(allDelegateInputRefAndOutParams, AsyncCallback cb, object state);
           public delegateReturnValue EndInvoke(allDelegateRefAndOutParams, IAsyncResult result);
       }

其中,Invoke()方法定义的参数和返回值与委托类型的定义完全相同;BeginInvoke()的参数由两部分组成,前面部分也基于委托类型的声明,后面两个参数用于异步方法调用;EndInvoke()方法的返回值与委托类型的声明相同,并且接受一个实现了IAsyncResult接口的对象作为唯一参数。
如果使用Delegate关键字,就间接创建了一个类,这个类“是”MulticastDelegate。它包含了达成委托的作用的方法。下面是所有委托类型都共有的核心成员:

   Method                  |  此属性返回System.Reflection.MethodInfo对象,用以表示委托维护的静态方法的详细信息
   Target                  |  如果方法调用是定义在对象级别的(而不是静态方法),Target返回表示委托维护的方法的对象。如果Target返回值为null,调用的方法是一个静态成员。
   Combine()               |  此静态方法给委托维护的列表添加一个方法。在C#中,使用重载 += 操作符作为简化符号调用此方法。
   GetInvocationList()     |  此方法返回一个System.Delegate类型的数组,数组中每个元素都表示一个可调用的特定方法。
   Remove(),  RemoveAll()  |  这些静态方法从调用列表中移除一个(或所有)方法。在C#中,Remove()方法可以通过使用重载 -= 操作符来调用。
5. 注册函数和调用方法列表

前面的例子只是为了使用委托而使用委托,如果只为了打印出三行字符串不必写这么多代码。委托被设计来应对这样的问题:在一个特定的时刻,我不能够或者不方便给出具体要执行的代码,但我仍然希望合适的代码会被运行。这个要求看起来很过分,但是如果在这个“特定的时刻”另外一个人很自然地就知道这些代码具体是什么样的,问题就变的很轻松。在这里,委托的效果类似于回调(call back)函数。(比如此时基于A的对象a正在运行,a希望在屏幕上打印出自己的状态,显然在编写类A时是不可能预见其对象内参数的具体值的。)
这样一来,委托类型是否是公共就不重要了,因为委托类型通常被定义在调用它的实例的类里。事实上,公共的委托成员变量是有风险的(公共的东西都有风险嘛),那么就需要一个封装服务来从外部注册委托实例(因为这时候已经不能直接访问委托类型变量了)。
注册函数就是做这样的事情的。它是一个公共的方法,可以从外部调用,它的作用是把外部提供的委托实例“注册”到使用委托的类中以供使用。下面是一个例子:

      public class Car
      {
          public int CurrentSpeed {get; set;}
          public int MaxSpeed {get; set;}
          public string PetName {get; set;}
          private bool carIsDead;
          public delegate void CarEngineHandler(string msgForCaller); // 定义委托类型
          private CarEngineHandler listOfHandlers; // 委托类型的变量
          public Car(string name, int maxSp, int currSp)
          {
              CurrentSpeed = currSp;
              MaxSpeed = maxSp;
              PetName = name;
          }
          public void RegisterWithCarEngine(CarEngineHandler mathodToCall) //这里的参数既是一个委托实例(CarEngineHandler),也是一个方法
          {
              listOfHandlers = methodToCall; // listOfHandlers变成了委托实例
          }
          public void Accelerate(int delta)
          {
              if (carIsDead)
              {
                  if (listOfHandlers != null) // 这个检查很重要!如果listOfHandlers为空而又试图调用它将会触发引用为空异常(NullReferenceException)!
                      listOfHandlers("Sorry, this car is dead..."); // 调用委托实例
              }
              else
              {
                  CurrentSpeed += delta;
                  if (10 <= (MaxSpeed - CurrentSpeed) && listOfHandlers != null) // 不要忘了检查 listOfHandlers != null
                  {
                      listOfHandlers("Careful buddy! Gonna blow!"); // 调用委托实例
                  }
                  if (CurrentSpeed >= MaxSpeed)
                      carIsDead = true;
                  else
                      Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
              }
          }
      }

调用Accelerate使车子加速时,就可能会调用委托实例。

     class Program
     {
         static void Main(string[] args)
         {
             Console.WriteLine("***** Delegate as event enablers *****\n");
             Car car1 = new Car("SlugBug", 100, 10);
             car1.RegisterWithCarEngine(new Car.EngineHandler(OnCarEngineEvent));
             Console.WriteLine("***** Speeding up *****");
             for(int i = 0; i < 10; i++)
                 car1.Accelerate(20);
             Console.ReadLine();
         }
         public static void OnCarEngineEvent(string msg) //委托实例的操作
         {
             Console.WriteLine("\n***** Message From Car Object *****");
             Console.WriteLine("=> {0}", msg);
             Console.WriteLine("**********************************\n");
         }
     }

一个委托对象可以维护一个调用方法的列表,也就是说可以对应多个方法。如前面4.叙述的,使用重载操作符+=向调用方法列表中增加方法,使用重载操作符-=从调用方法列表中移除一个方法。下面是对应的注册和注销函数:

     public void RegisterWithCarEngine(CarEngineHandler methodToCall)
     {
         listOfHandlers += methodToCall;
     }
     public void UnregisterWithCarEngine(CarEngineHandler methodToCall)
     {
         listOfHandlers -= methodToCall;
     }

这样在调用委托实例时,会调用注册在listOfHandlers中的所有方法。

6. 更灵活的委托

委托的一个重要特点是类型安全。当声明一个委托类型时,规定了返回值与参数列表。当试图使用委托而返回与声明不符的值,或是传入与声明不符等参数时,编译器会报错。这在提供安全性的同时也带来了限制,在下面两种情况下,我们希望能够有更便捷的编码方式,这就产生了委托协变和泛型委托。

a. 能够指向相关继承体系的委托
 委托是安全类型,它不遵守继承的基本规则。如果指向两个类型的返回值,尽管它们之间有继承关系,还是只能定义两个委托。但是在.net2.0之后,委托协变使只使用一个委托成为了可能:
       public delegate Car ObtainVehicleDelegate();
       public static Car GetBasicCar() { return new Car(); }
       public static SportsCar GetSportsCar() { return new SportsCar(); } //SportsCar继承自Car
       static void Main(string[] args)
       {
           Console.WriteLine("***** Delegate Convariance *****\n");
           ObtainVehicleDelegate targetA = new ObtainVehicleDeleagte(GetBasicCar); // 返回值一致,可以执行
           Car c = targetA();
           Console.WriteLine("Obtained a {0}", c);
           ObtainVehicleDelegate targetB = new ObtainVehicleDeleagte(GetSportsCar); //返回值在同一继承体系,可以执行
           SportsCar sc = (SportsCar)targetB(); // 委托的返回值仍然是Car类型,需要做一个强制转换
           Console.WriteLine("Obtained a {0}", sc);
           Console.ReadLine();
       }

类似的,逆变(contravariance)允许委托指向多个方法,这些方法的参数是存在传统继承关系的对象。

b. 能够接受任何类型参数的委托

委托可以是泛型的,这样就能够接受任何类型的参数。

       public delegate void MyGenericDelegate<T> (T arg); //这个泛型委托可以接受任何返回void的单个参数方法
       class Program
       {
           static void Main(string[] args)
           {
               Console.WriteLine("***** Generic Delegates *****\n");
               MyGenericDelegate<string> strTarget = new MyGenericDelegate<string>(StringTarget); // 这个委托实例可以接受string类型
               strTarget("Some string data");
               MyGenericDelegate<int> intTarget = new MyDenericDelegate<int>(IntTarget); // 这个委托实例可以接受int类型的参数
               intTarget(9);
               Console.ReadLine();
           }
           static void StringTarget(string arg)
           {
               Console.WriteLine("arg in uppercase is: {0}", arg.ToUpper());
           }
           static void IntTarget(int arg)
           {
               Console.WriteLine("++arg is {0}", ++arg);
           }
       }
7. 方法组转换语法

在声明了一个委托类型后,它所需要对签名是固定的,使用委托对象通常只是为了传递一个方法名称。针对这一特点,C#提供一个语法——方法组转换语法——以简化操作,使用方法组转换语法后可以不创建委托对象而直接提供它的名称。

     class Program
     {
         static void Main(string[] args)
         {
             Console.WriteLine("***** Method Group Conversion *****\n");
             Car c1 = new Car();
             c1.RegisterWithCarEngine(CallMeHere); //省去创建委托对象,直接注册
             Console.WriteLine("***** Speeding up *****");
             for(int i=0; i<6; i++)
                 c1.Accelerate(20);
             c1.UnregiterWithCarEngine(CallMeHere); // 同样地注销委托实例
             for(i=0; i<6; i++)
                 c1.Accelerate(20);
             Console.ReadLine();
         }
         static void CallMeHere(string msg)
         {
             Console.WriteLine("=> Message from Car: {0}", msg);
         }
     }
8. 重新考察委托调用列表和注册/注销函数

委托调用列表和注册/注销函数都是为了封装委托变量,使外部不能直接访问它而创造出来的。
但是它们并没有很好地完成这个任务。这三者都是由程序员自己码出的功能部件,并不是.net平台或C#语言自身提供的,这就带来了不确定和不便捷。一方面,如果把委托调用列表写成公共的,外部还是可以直接调用它,它就没有任何意义;另一方面,注册/注销函数使用起来也不方便,总是需要码较多的代码而实现很简单的功能。
实际上,真正完成这个封装的是事件,但是为了更好地理解事件机制,先来分析一下委托调用列表和注册/注销函数:

委托调用列表并不是一个真正的list,而是一个委托对象。
当声明了一个委托类型后,我们就把它只是当作一个类型来用(尽管实际上编译器为我们创建了一个MulticastDelegate对象),而“委托调用列表”就是这个类型的成员变量。实际上,我们是用“委托调用列表”来代指了实际维护委托功能的那个密封类。 注册/注销函数只是用来间接地调用Combine和Remove函数
注册/注销函数的内容只有一行,其中的+=/-=操作符实际上是调用了MultiCastDelegate类里的Combine和Remove函数。 9. event关键字

C#提供了event这个关键字用于取代一系列的add/remove方法。当编译器处理event关键字时,它自动地创建了add_NameOfEvent和remove_NameOfEvent两个方法,这两个方法实现了对Delegate.Combine和Delegate.Remove方法的调用。也就是说,事件做了委托调用列表和注册/注销函数的事情:

     public class Car
     {
         public delegate void CarEngineHandler(string msg);
         public event CarEngineHandler Exploded;
         public event CarEngineHandler AboutToBlow;
         ...
         public void Accelerate(int delta)
         {
             if(carIsDead)
             {
                 if(Exploded != null)
                     Exploded("Sorry. this car is dead..."); // 这里依旧要检查事件是否为空啊!
             }
             else
             {
                 CurrentSpeed += delta;
                 if(10 <= MaxSpeed - CurrentSpeed && AboutToBlow != null) // 千万别忘了啊!
                 {
                     AboutToBlow("Careful buddy! Gonna blow!");
                 }
                 if(CurrentSpeed >= MaxSpeed)
                     carIsDead = true;
                 else
                     Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
             }
         }
     }

使用事件的好处不止在于简化和封装,现在,对于定义了委托的类来说,委托实例们都有了名字,这利于码农更好地思考这些委托都是用来干什么的。所谓委托,即是使程序有在某个时间做某件事情的能力,而这个“某件事”则是事件。
下面来看怎样调用这些事件:

     static void Main(string[] args)
     {
         Console.WriteLine("***** Fun with Events *****\n");
         Car c1 = new Car("SlugBug", 100, 10);
         // 注册处理事件的程序,现在可以直接调用事件了
         c1.AboutToBlow += CarIsAlmostDoomed;
         c1.AboutToBlow + CarAboutToBlow;
         c1.Exploded += CarExploded;
         // 注销处理事件的程序
         Console.WriteLine("***** Speedng up *****\n");
         for(int i=0; i<6; i++)
             c1.Accelerate(20);
         c1.Exploded -= CarExploded;
         for(i=0; i<6; i++)
             c1.Accelerate(20);
         Console.ReadLine();
     }
10. 事件机制

C#的事件机制主要由System.EventArgs和System.EventHandler维护。其中EventArgs是基类,EventHandler是委托。
EventArgs是所有事件类的基类,创建继承自EventArgs的类并提供存储必要数据的属性就能创建一个自定义的事件类。自定义的事件以EventArgs作为名称的尾部。
MSDN在EventHandler委托里面备注了一段关于the event model的内容讲得非常清楚:

The event model in the .NET Framework is based on having an event delegate that connects an event with its handler. To raise an
event, two elements are needed:

    - A delegate that identifies the method that provides the response to the event.
    - Optionally, a class that holds the event data, if the event provides data.

The delegate is a type that defines a signature, that is, the return value type and parameter list types for a method. You can use
the delegate type to declare a variable that can refer to any method
with the same signature as the delegate.
The standard signature of an event handler delegate defines a method that does not return a value. This method’s first parameter is
of type T:System.Object and refers to the instance that raises the
event. Its second parameter is derived from type T:System.EventArgs
and holds the event data. If the event does not generate event data,
the second parameter is simply the value of the
F:System.EventArgs.Empty field. Otherwise, the second parameter is a
type derived from T:System.EventArgs and supplies any fields or
properties needed to hold the event data.
The T:System.EventHandler delegate is a predefined delegate that specifically represents an event handler method for an event that does
not generate data. If your event does generate data, you must use the
generic T:System.EventHandler`1 delegate class.
To associate the event with the method that will handle the event, add an instance of the delegate to the event. The event handler
is called whenever the event occurs, unless you remove the delegate.

大概翻译如下:
.NET框架下的事件模型基于一个把事件和它的handler(貌似是翻译成句柄,但其实一般我们也不懂中文的句柄是个啥意思。。。)联系起来的事件委托。创建一个事件需要两个要素:

  - 一个委托,用于指出响应事件的方法
  - 一个包含事件数据的类,如果事件有数据(需要传递),那么就需要有个类能够承载这些数据。

委托是一个定义了签名(signature)的类型(type),这里的签名是指方法所具有的返回值类型和参数列表类型。你可以用委托类型来声明一个变量,这个变量可以指代任何与委托有相同签名的方法。
(通常来说)一个event handler委托的标准签名定义了一个返回值为void的方法。这个方法的第一个参数是System.Object类的,它表示事件的发出者;第二个参数继承自System.EventArgs,包含了事件(要传递)的数据。如果事件不需要传送数据,第二个参数应该是System.EventArgs.Empty字段的值。另外,第二个参数是一个继承自System.EventArgs的类型,并且提供了用于承载事件数据的字段或属性。
System.EventHandler委托是一个预定义了的委托,它只相当于一个不产生数据的事件的event handler方法。如果你的事件要产生数据,你必须使用普通的System.EventHandler委托类。
为事件的委托增加一个实例就把事件和一个handle它的方法联系了起来,只要你不删去委托,当事件发生时这个方法就会被调用。
现在可以修改前面的例子:

      public class CarEventArgs : EventArgs
      {
          public readonly string msg;
          public CarEventArgs(string message) { msg = message; }
      }
      public class Car
      {
          public delegate void CarEngineHandler(object sender, CarEventArgs e);
          ...
          public void Accelerate(int delta)
          {
              if(carIsDead)
              {
                  if (Exploded != null)
                      Exploded(this, new CarEventArgs("Sorry, this cat is dead...");)
              }
          }
      }
      public class Program
      {
          public static void CarAboutToBlow(object sender, CarEventArgs e)
          {
            if(sender is Car) // 为了安全,在强制转换之前要检查!
            {
            Car c = (Car)sender;
            Console.WriteLine("Critical Message from {0}: {1}", c.PetName, e.msg);
            }
          }
      }

最新发布

CentOS专题

关于本站

5ibc.net旗下博客站精品博文小部分原创、大部分从互联网收集整理。尊重作者版权、传播精品博文,让更多编程爱好者知晓!

小提示

按 Ctrl+D 键,
把本文加入收藏夹