工做十余年,仍是一直被问 委托和事件 有什么区别? 真是够了

一:背景

1. 讲故事

前几天公司一个妹子问我,事件和委托有什么区别? 先由衷感叹一下,编码十余年,年轻的时候常被面试官问起,如今年长了,却被后辈们时常问候,看样子逃离编码生涯以前是跑不掉了,不过奇怪的是,这个问题被问起的时候,我发现有不少人用: 事件是一种特殊的委托 来进行总结,是否是挺有意思,我想这句话可能来自于网络上的面试题答案吧,这篇我就试着完全总结一下。前端

二:事件真的是特殊的委托吗?

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,若是你们没有看懂能够多看几遍哈。redis

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<Action> 数组,模拟 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 + "正在逃跑");
        }
    }

看到这里,我想你对 事件和委托 应该有一个大概的认识了吧,但这里还有一个问题,C#中的事件 真的如我写的观察者模式这样的吗??? 要回答这个问题,须要从 IL 角度看一下事件到底生成了什么。框架

三:从IL角度看事件

1. 使用 ilspy /ildasm 小工具

首先来看一下所谓的事件到底在 IL 层面是个什么东西,以下图:ide

从图中看其实就是两个接收 Action 参数的 add_CatComeremove_CatCome方法,这两个方法简化后的 il 代码以下:工具

.event [mscorlib]System.Action CatCome
{
	.addon instance void ConsoleApp2.Cat::add_CatCome(class [mscorlib]System.Action)
	.removeon instance void ConsoleApp2.Cat::remove_CatCome(class [mscorlib]System.Action)
}

.method public hidebysig specialname 
	instance void add_CatCome (
		class [mscorlib]System.Action 'value'
	) cil managed 
{
	// Method begins at RVA 0x2090
	// Code size 41 (0x29)
	.maxstack 3
	.locals init (
		[0] class [mscorlib]System.Action,
		[1] class [mscorlib]System.Action,
		[2] class [mscorlib]System.Action
	)

	IL_0000: ldarg.0
	IL_0001: ldfld class [mscorlib]System.Action ConsoleApp2.Cat::CatCome
	IL_0006: stloc.0
	// loop start (head: IL_0007)
		IL_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
		IL_0010: castclass [mscorlib]System.Action

		IL_0017: ldflda class [mscorlib]System.Action ConsoleApp2.Cat::CatCome

		IL_001e: call !!0 [mscorlib]System.Threading.Interlocked::CompareExchange<class [mscorlib]System.Action>(!!0&, !!0, !!0)

	// end loop
	IL_0028: ret
} // end of method Cat::add_CatCome

.method public hidebysig specialname 
	instance void remove_CatCome (
		class [mscorlib]System.Action 'value'
	) cil managed 
{
	IL_0000: ldarg.0
	IL_0001: ldfld class [mscorlib]System.Action ConsoleApp2.Cat::CatCome
	IL_0006: stloc.0
	// loop start (head: IL_0007)
		IL_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Remove(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
		IL_0010: castclass [mscorlib]System.Action

		IL_0017: ldflda class [mscorlib]System.Action ConsoleApp2.Cat::CatCome

		IL_001e: call !!0 [mscorlib]System.Threading.Interlocked::CompareExchange<class [mscorlib]System.Action>(!!0&, !!0, !!0)

		IL_0026: bne.un.s IL_0007
	// end loop
	IL_0028: ret
} // end of method Cat::remove_CatCome

接下来看看 mouse 类的注册是怎么实现的。oop

从图中能够看到,所谓的注册就是将 RunAway 做为 add_CatCome 方法的参数传进去而已,回过头来看,最核心的就是那两个所谓的 addxxxremovexxx 方法。

2. 将IL代码进行C#还原

可能有些同窗对 IL 代码不是很熟悉,若是能还原成 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> 集合,可是你仔细分析的话,实际上是有的,你必定要着重分析这句代码: Action value2 = (Action)Delegate.Combine(action2, value); 它用的就是多播委托,用 Combine 方法将后续的 Action 送到前者Action的 _invocationList 中,不信的话,我调试给你看哈。

没毛病吧, Action CatCome 中已经有了两个 callback 方法啦,一旦 CatCome.Invoke(), _invocationList 中的方法就会被执行,也就看到两只老鼠在逃跑啦。

四: 总结

您如今是否是明白啦,委托和事件的关系 比如 砖头和房子的关系,房子只是砖头的一个应用场景,您若是说房子是一种特殊的砖,这句话品起来是否是有一种怪怪的感受,不是吗?

如您有更多问题与我互动,扫描下方进来吧~

图片名称