C# 委托(二)—— 多播委托与事件
目录
上一篇文章中,介绍了委托的基本用法(传送门: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 代表着自定义事件的详细信息类。我们来看该委托的定义:
从定义中可看出,事件委托采用了两个参数: 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)可以直接使用一个方法为委托赋值,而事件只开放了 += 和 -= 运算符为其添加或删除方法。