欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C# 委托(二)—— 多播委托与事件

程序员文章站 2022-05-06 09:43:08
...

目录

1 多播委托

1.1 多播委托的实例化

1.2 多播委托的调用与返回结果

1.3 多播委托的逐个调用

2 事件

2.1 事件的本质

2.2 事件用法三步曲

2.3 事件与多播委托的区别


上一篇文章中,介绍了委托的基本用法(传送门:https://blog.csdn.net/wnvalentin/article/details/81840339)。本文中,我们来了解一下常用的事件机制,以及事件的基石——多播委托。

1 多播委托

所谓多播委托,即 “多路广播委托”(MulticastDelegate)。从它的名字就可以看出,此种委托可以像广播一样将影响信息“传播”到四面八方。多播委托类拥有一个方法调用列表,调用委托时,它就会逐一调用该列表中的方法,从而实现多重影响。

MulticastDelegate 位于 System 命名空间下,它派生于 Delegate 类,声明、调用方法与普通委托相同,但是实例化方法不太一样。

1.1 多播委托的实例化

多播委托的初始化可以像普通委托一样,传入一个签名相同的实例方法。同时,多播委托重载了 += 运算符和 -= 运算符,用来向其调用列表中添加或者删除方法。调用多播委托时,方法将按照添加的顺序被依次调用。

我们结合案例进行说明。

我们定义一个简单的执行加减运算的委托,并利用+=运算符和Lambda表达式为该多播委托添加两个方法:

public delegate int AddDelegate(int a);

AddDelegate myAddDelegate = new AddDelegate(i => i + 10);
myAddDelegate += i => i - 10;

 看到这里,细心的同学可能有个问题了,我们声明的是个普通的委托,为啥能当多播委托来实例化呢?实际上,普通的Delegate类用途不是很大,而我们 自定义的委托,以及常用的内置委托如Func<T>,都是派生于多播委托的!我们可以验证一下:

输入

var type = typeof(AddDelegate);
Console.WriteLine($"AddDelegate委托的基类为{type.BaseType.FullName}");

输出为:

AddDelegate委托的基类为System.MulticastDelegate

1.2 多播委托的调用与返回结果

调用上面的AddDelegate,传入一个参数10:

Console.WriteLine(myAddDelegate?.Invoke(10));

 注意多播委托的调用,由于调用列表中的方法可能被 -= 运算符全部删除,为了避免调用空委托,需要用?.进行调用。

运行上述代码,得到结果:

0

 奇了怪了,不是有两个方法么?怎么只有一个返回值呢???

实际上,两个方法都被调用了。我们来分析一下:多播委托按照顺序调用其列表中的方法。本例中,首先,我们对参数10调用了  i => i + 10  函数,得到了本匿名函数返回值20。然而,委托的调用并没有停下来, 而是继续调用剩余的方法。然后继续对参数10调用   i => i - 10  函数,得到新的返回值0,上个函数的返回值被覆盖丢弃。至此委托调用结束,返回最后调用方法的返回结果

因此,一个具有非空返回值的多播委托通常是没有意义的,因为只能获得最后一个方法的返回结果。通常,多播委托的返回类型为 void。

1.3 多播委托的逐个调用

那么对于返回类型不为空的多播委托来说,有没有办法得到所有方法的返回结果呢?有的!多播委托提供了一个 GetInvocationList () 方法,通过它可按顺序获取并执行调用列表中的方法。用法举例:

//逐个调用
Console.WriteLine("逐个调用:");
foreach (AddDelegate f in myAddDelegate.GetInvocationList())
    Console.WriteLine(f.Invoke(10));

 如此便可逐个得到方法的输出结果:

逐个调用:
20
0

2 事件

事件机制是基于多播委托的。理解了多播委托,事件就不难理解。

2.1 事件的本质

事件是一种委托,具体的说来,事件是一种名为 EventHandler<TEventArgs>  的泛型委托。它是.NET为我们实现事件而专门提供的委托类(微软大法好)。其中的泛型类型 TEventArgs 代表着自定义事件的详细信息类。我们来看该委托的定义:

C# 委托(二)—— 多播委托与事件

从定义中可看出,事件委托采用了两个参数: sender 和 泛型参数 TEventArgs。其中 sender 代表事件源,是object类型的,所以我们可以传入任何自定义的事件触发对象。第二个参数就是实例化该泛型委托时时传入的实际类型,代表着事件参数,它必须派生于 EventArgs 类,我们可以建立这个事件参数类,通过为该类添加自定义属性来加入任何你想要的事件信息。

2.2 事件用法三步曲

事件机制的使用方法可以归纳为3个步骤:

(1)事件发布者定义event以及事件相关信息  (2)事件侦听者订阅event   (3)事件发布者触发event,自动调用订阅者的事件处理方法。

下面用案例来说明。构建一个场景:作为汽车经销商,当有新车到店时,会发布一个事件,通知订车的消费者,告诉他们汽车的相关信息,消费者接收事件通知后,进行相应的处理。这里,事件的发布者就是汽车经销商,事件的订阅者就是消费者。

1. 事件发布者定义event以及事件相关信息

作为事件发布者,可以首先定义自己的事件参数类 TEventArgs:

public class CarInfoEventArgs : EventArgs
{
     public string Car { get; }
     public CarInfoEventArgs(string car)
     {
          Car = car;
     }
}

这里我们给自定义事件参数类中加入了汽车的信息。

然后定义我们的经销商类 CarDealer,它拥有一个事件成员:

public class CarDealer
{
     public event EventHandler<CarInfoEventArgs> NewCarEvent;   
}

 事件用 event 关键字来定义。Event 是对多播委托的一种封装。详见下文。事件一般作为事件发布者的一个成员,定义在发布者内部。

2. 事件订阅者(消费者)订阅事件

现在我们定义一个消费者类,它拥有一个事件处理方法 KnowsNewCarArrived,事件的订阅就是将该方法添加到事件的调用列表里。

public class Consumer
{
     private readonly string _name;
     public Consumer(string name)
     {
          _name = name;
     }

     public void KonwsNewCarArrived(object sender, CarInfoEventArgs e)
     {
          Console.WriteLine($"  {_name}: OK, I learn that car {e.Car} arrived.");
     }
}

现在我们在客户端中创建两个消费者对象,并订阅新车到达的事件:

var dealer = new CarDealer();
var Tom = new Consumer("Mike");
var Mary = new Consumer("Mary");

dealer.NewCarEvent += new EventHandler<CarInfoEventArgs>(Tom.KonwsNewCarArrived);
dealer.NewCarEvent += Mary.KonwsNewCarArrived;

3. 事件发布者触发事件

事件是由事件发布者(经销商)触发的,为了触发新车到达事件,我们 给 CarDealer 类添加一个 NewCarArrives 方法,该方法调用事件的 Invoke() 方法,并传入事件参数。

public class CarDealer
{
     public event EventHandler<CarInfoEventArgs> NewCarEvent;

     public void NewCarArrives(string car)
     {
         Console.WriteLine($"CarDealer: Attention, new car {car} arrives!!!");
         NewCarEvent?.Invoke(this, new CarInfoEventArgs(car));
     }
}

 最后在客户端调用该方法,实现事件的触发:

dealer.NewCarArrives("DasAuto");

触发事件后,注册到事件中的消费者方法被依次调用,输出如下:

CarDealer: Attention, new car DasAuto arrives!!!
  Mike: OK, I learn that car DasAuto arrived.
  Mary: OK, I learn that car DasAuto arrived.

与多播委托一样,也可通过 -= 运算符取消订阅事件。

2.3 事件与多播委托的区别

前面说,事件就是多播委托的一种封装。那么,如果不使用事件,直接使用委托,能不能实现事件机制呢?答案是完全可以。读者可声明一个 public Action<object,CarInfoEventArgs> NewCarDelegate 委托来代替案例中的 EventHandler 事件,其他处理与事件完全相同,可实现相同的结果。

那么,为什么还要用事件呢?事件是怎么封装了委托呢?

实际上,在以上案例中,使用 event 关键字在一行上定义事件是C# 提供的事件的简化记法。在我们定义了上述事件后,编译器会自动生成如下代码:

private EventHandler<CarInfoEventArgs> newCarEvent;
public event EventHandler<CarInfoEventArgs> NewCarEvent
{
    add
    {
        newCarEvent += value;
    }
    remove
    {
        newCarEvent -= value;
    }
}

这非常像字段及其属性的关系。注意到,委托字段 newCarEvent 是私有的,因此在外部不能直接为事件赋值,但可以通过公开的 += 和 -= 运算符为事件添加实例方法。 另外,事件 event 是一种数据类型,是一个已经声明的委托类,只能在某个类的内部声明,并且只能被该类调用,不能在命名空间中声明和使用。这一点与委托 delegate 的声明不同。

总结:事件与委托的区别在于两点:

(1)委托是一个类,可以在命名空间中声明;而事件只能在事件发布者内部定义,且只能被该类调用。

(2)可以直接使用一个方法为委托赋值,而事件只开放了 += 和 -= 运算符为其添加或删除方法。