文章目录
1、背景
- 事件是一种特殊的委托,委托类似C++中的函数指针、这个指针指向函数、经常在百度上看到这样的解答,下面详细总结一下为什么这样说。
2、事件真的是特殊的委托吗?
2.1、猫和老鼠的经典案例
class Program
{
static void Main(string[] args)
{
Cat cat = new Cat("汤姆");
Mouse mouse1 = new Mouse("杰瑞", cat);
Mouse mouse2 = new Mouse("杰克", cat);
cat.CatComing();
Console.ReadKey();
}
}
class Cat
{
public event Action CatCome; //声明一个事件
private string name;
public Cat(string name)
{
this.name = name;
}
public void CatComing()
{
Console.WriteLine("猫" + name + "来了");
CatCome?.Invoke();
}
}
class Mouse
{
private string name;
public Mouse(string name, Cat cat)
{
this.name = name;
cat.CatCome += this.RunAway; //Mouse 注册 CatCome 主题
}
public void RunAway()
{
Console.WriteLine(name + "正在逃跑");
}
}
- 代码非常简洁,猫的 CatCome 动作一旦触发,注册到 CatCome 上的 两只 mouse 就会执行各自的逃跑动作 RunAway。
2、观察者模式/发布订阅模式
- 如果你了解过设计模式,我想你应该第一眼就能看出这是 观察者模式,对的,现在无数的框架都在使用这个模式,比如前端的:Vue,Knockout,React,还有redis的发布订阅等等,如果用图画一下大概就是这样。
- 从图中可以看到,几个 subscribe 都订阅了一个叫做 subject 的主题,一旦有外来的 publish 推送到了 subject,那么订阅 subject 的 subscribe 都会收到通知,接下来根据这张图对刚才的代码再缕一篇:
- 猫的 public event Action CatCome 就是一个主题 (subject)。
- 老鼠的 cat.CatCome += this.RunAway 就是 subscribe 对 subject 的订阅。
- 最后的 public void CatComing() 就是对 subject 的推送, pubish了一条 猫来了。
3、使用观察者模式 对 猫鼠进行解剖
- 有了观察者模式的基础,对上面的代码进行改造就方便多了, 我可以把 public event Action CatCome; 改成 一个 List 数组,模拟 Subject 哈,简化后的代码如下:
class Cat
{
public List<Action> Subject = new List<Action>(); //定义一个主题
private string name;
public Cat(string name)
{
this.name = name;
}
public void CatComing()
{
Console.WriteLine("猫" + name + "来了");
Subject.ForEach(item => { item.Invoke(); });
}
}
class Mouse
{
private string name;
public Mouse(string name, Cat cat)
{
this.name = name;
cat.Subject.Add(RunAway); //将 逃跑 方法注入到 subject 中
}
public void RunAway()
{
Console.WriteLine(name + "正在逃跑");
}
}
- 看到这里我对 事件和委托 有一个大概的认识了。
4、从IL角度看事件
4.1、使用 ilspy /ildasm 小工具
- 首先来看一下所谓的事件到底在 IL 层面是个什么东西,如下图:
- 从图中看其实就是两个接收 Action 参数的 add_CatCome和 remove_CatCome方法,这两个方法简化后的 il 代码如下:
看看 mouse 类的注册是怎么实现的。
- 从图中可以看到,所谓的注册就是将 RunAway 作为 add_CatCome 方法的参数传进去而已,回过头来看,最核心的就是那两个所谓的 addxxx 和 removexxx 方法。
4.2、将IL代码进行C#还原
- 能还原成 C# 代码就🐂👃了,接下来就试着还原一下。
class Cat
{
Action CatCome;
public void add_CatCome(Action value)
{
Action action = this.CatCome;
Action action2 = null;
do
{
action2 = action;
Action value2 = (Action)Delegate.Combine(action2, value);
action = Interlocked.CompareExchange(ref this.CatCome, value2, action2);
}
while ((object)action != action2);
}
public void remove_CatCome(Action value)
{
Action action = this.CatCome;
Action action2 = null;
do
{
action2 = action;
Action value2 = (Action)Delegate.Remove(action2, value);
action = Interlocked.CompareExchange(ref this.CatCome, value2, action2);
}
while ((object)action != action2);
}
private string name;
public Cat(string name)
{
this.name = name;
}
public void CatComing()
{
Console.WriteLine("猫" + name + "来了");
CatCome?.Invoke();
}
}
class Mouse
{
private string name;
public Mouse(string name, Cat cat)
{
this.name = name;
cat.add_CatCome(this.RunAway);
}
public void RunAway()
{
Console.WriteLine(name + "正在逃跑");
}
}
- 可以看出还原后的C#代码跑起来是没有问题的,和观察者模式相比,这里貌似没有看到 subject 这样的 List 集合,但是你仔细分析的话,其实是有的,你一定要着重分析这句代码: Action value2 = (Action)Delegate.Combine(action2, value); 它用的就是多播委托,用 Combine 方法将后续的 Action 送到前者Action的 _invocationList 中,不信的话,我调试给你看哈。
- 没毛病吧, Action CatCome 中已经有了两个 callback 方法啦,一旦 CatCome.Invoke(), _invocationList 中的方法就会被执行,也就看到两只老鼠在逃跑啦。
5、总结
- 您现在是不是明白啦,委托和事件的关系 好比 砖头和房子的关系,房子只是砖头的一个应用场景,您如果说房子是一种特殊的砖,这句话品起来是不是有一种怪怪的感觉,不是吗?