返回「计算机、信息技术与工程」

深入理解委托和事件

更多
Markdown 结构化数据
本文目录 12 个章节

深入理解委托和事件

创建时间:2020/9/1 11:01

  • 深入理解委托和事件
    • 总述
    • 事件
    • C#委托与C++函数指针
    • Delegate
      • 探索委托的底层实现机制
        • 示例
        • Delegate, MulticastDelegate, delegate三者的关系详解
        • Combine函数
        • 总结
        • 用法示例
    • Event
    • Ref

总述

  1. 事件的声明只是在委托前面加一个event关键词,虽然你可以定义一个public,但是有了event关键词后编译器始终会把这个委托声明为private,然后添加1组add,remove方法。add对应+=,remove对应-=。这样就导致事件只能用+=,-=来绑定方法或者取消绑定方法。而委托可以用=来赋值,当然委托也是可以用+=,-=来绑定方法的(面试我的那个哥们好像说不行)。

  2. 委托可以在外部被其他对象调用,而且可以有返回值(返回最后一个注册方法的返回值)。而事件不可以在外部调用,只能在声明事件的类内部被调用。我们可以使用这个特性来实现观察者模式。大概就是这么多。下面是一段测试代码。

事件

事件 是软件系统里的两个子系统之间,或者两个模块之间,或者两个对象之间发送消息,并处理消息的过程。在面向对象的世界里,就可以统一认为是两个对象之间的行为。 两个对象之间发送的这种消息,对发送方来讲是产生一个事件,对接受方来讲是需要处理某个事件。这种消息可以是用户操作产生的或者软件系统里的某个对象产生的。

事件机制 :对象一产生一个事件,这个事件发生以后需要对象二执行某种动作。这就是事件机制。 对象一是事件的 产生者 ,或者 发送者 ;对象二是事件的 接收者 或者 订阅者

C#委托与C++函数指针

  • 函数指针只能引用静态函数,而委托可以引用静态方法和实例方法。

  • 当委托引用实例方法时,委托不仅存储对方法入口点的引用,还存储对为其调用该方法的类实例的引用。

  • 与函数指针不同,委托是面向对象、类型安全并且安全的。

Delegate

委托 是一个 ,它定义了 方法的类型 ,使得可以 将方法当作另一个方法的参数来进行传递 ,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If … Else(Switch)语句,同时使得程序具有更好的可扩展性。 可以将多个方法绑定到同一个委托,当调用这个委托的时候,将依次调用其所绑定的方法。

  • 第一次用的“=”,是赋值的语法;第二次,用的是“+=”,是绑定的语法。如果第一次就使用“+=”,将出现“使用了未赋值的局部变量”的编译错误。
string str1 = "test";
string str2 = "test";

探索委托的底层实现机制

示例

笔者书写了与delegate的基本使用相关的代码,并对编译后的结果进行查看。该章节展示Demo。后文将对其进行分析解读。

原始脚本

string str1 = "test";
string str2 = "test";

经过编译后再反编译的结果——c#

string str1 = "test";
string str2 = "test";

Delegate, MulticastDelegate, delegate三者的关系详解

Delegate类是委托类型的基类。 但是,只有系统和编译器才能显式从 Delegate 类或 MulticastDelegate 类派生。 也不允许从委托类型派生新类型。 Delegate类 不被视为委托类型 ; 它是用于派生委托类型的类。 Delegate类实质上没有实现_invocationList(调用列表),不支持注册并调用多个方法。

MulticastDelegate(多路广播委托):是一个继承于Delegate的类,其拥有一个带有链表格式的委托列表,该列表称为调用列表,在调用多路广播委托时,将按照调用列表中的委托出现的顺序来同步调用这些委托。

平常我们声明一个delegate的类型,都是继承于MulticastDelegate类的。 如以下案例所示,我们声明一个delegate。

string str1 = "test";
string str2 = "test";

相当于自定义了一个派生自MulticastDelegate的类,编译成IL代码之后,查看反编译结果如下:

string str1 = "test";
string str2 = "test";

Combine函数

+= 的实现: 实际上是调用Delegate.Combine函数。

string str1 = "test";
string str2 = "test";

使用dnSpy查看反编译的IL语言程序集,只能看到Combine函数的调用,看不到+=运算符的实现,由此推测,Mono在对C#脚本进行编译时,就直接将+=运算符编译成Combine函数。

string str1 = "test";
string str2 = "test";

Delegate类的Combine的实现

string str1 = "test";
string str2 = "test";

对于委托类型,执行a += b;运算,在Combine的实现上,如果a为null,则返回b。因此,Delegate类的对象,即使为null(不进行初始化),也可以使用+=运算进行注册。

从源码上看,Delegate类实质上没有实现CombineImpl函数。 语句a.CombineImpl(b);调用的函数,取决于a的类型

上文已经阐述到,我们定义一个delegate的类型,实质上就是定义了一个派生自MulticastDelegate的类。 因此,a.CombineImpl(b);语句,就是MulticastDelegate类的对象对CombineImpl函数的调用。

在MulticastDelegate类的CombineImpl函数。

string str1 = "test";
string str2 = "test";

总结

对以下代码的原理做个总结:

string str1 = "test";
string str2 = "test";

MulticastDelegate是一个回调函数的包装类。每个MulticastDelegate,包含一个数组,数组记录了多个Delegate。

使用关键字delegate定义一个委托类,实质上是定义了一个派生自MulticastDelegate类的委托类(自定义的Class)。 MyDelegateClass myDelegate;语句声明一个自定义的MyDelegateClass委托类的对象。 对该对象进行+=运算,Mono的编译器会将它处理成对Delegate.Combine(myDelegate,CallBackMethod)函数的调用。而Combine函数在实现上会根据参数myDelegate的类型,调用CombineImpl函数,由于myDelegate是一个MulticastDelegate类,因此,实际调用的就是MulticastDelegate类的成员函数CombineImpl。 CombineImpl会把MulticastDelegate对象的数据合并到 += 左边的MulticastDelegate对象。 一旦执行Invoke,便调用所有的回调函数。

用法示例

MulticastDelegate和Delegate的用法实例:

string str1 = "test";
string str2 = "test";

Event

委托存在的问题:

  • 权限问题,在Publisher中声明委托,若权限为public,则订阅者可以直接调用委托,影响到所有注册了该委托的订阅者,它给了客户端不应具有的能力。而使用事件,可以限制这一能力,更精确地对类型进行封装。

  • 方法注册时的语法问题:使用委托,第一个方法注册用“=”,是赋值语法,因为要进行实例化,第二个方法注册则用的是“+=”。但是,不管是赋值还是注册,都是将方法绑定到委托上,除了调用时先后顺序不同,再没有任何的分别,这样会有些别扭。

Event :封装了委托类型的变量,使得:在类的内部,不管你声明它是public还是protected,它总是private 的,。在类的外部,注册“+=”和注销“-=”的访问限定符与你在声明事件时使用的访问符相同。

Event只能在所声明的类的内部调用,在别的类中无法调用,但可以进行+=和-=操作

event关键字,使用使用属性对字段进行封装。声明一个事件不过类似于声明一个进行了封装的委托类型的变量而已。

string str1 = "test";
string str2 = "test";

委托和事件的案例:

string str1 = "test";
string str2 = "test";

上面代码使用了委托,没有使用事件。当使用委托变量时,客户端可以直接通过委托变量触发事件,也就是直接调用pub.NumberChanged(100),这将会影响到所有注册了该委托的订阅者。而事件的本意应该为在事件发布者在其本身的某个行为中触发,比如说在方法DoSomething()中满足某个条件后触发。

通过添加event 关键字来发布事件,事件发布者的封装性会更好,事件仅仅是供其他类型订阅,而客户端不能直接触发事件(语句pub.NumberChanged(100)无法通过编译),事件只能在事件发布者Publisher 类的内部触发(比如在方法pub.DoSomething()中),换言之,就是NumberChanged(100)语句只能在Publisher 内部被调用。

当你使用了event 关键字之后,直接在客户端触发事件这种行为,也就是直接调用pub.NumberChanged(100),是被禁止的。

还有一个约定俗称的规定,就是订阅事件的方法的命名,通常为“On 事件名”,比如这里的OnNumberChanged。

事件机制代码示例

string str1 = "test";
string str2 = "test";

总结:事件发布者发布的 事件 在实质上可以看成 对外提供的回调函数指针列表 。这个列表的容量可以 动态增长 。事件订阅者可以把自己的事件 注册 到这个列表或者撤销 注册 ,但是它从 原则上无法更改或者对其它订阅者的注册产生影响 。事件发布者通过两种手段使得订阅者正确地使用事件机制:

  • 一是定义一种delegate委托类型, 事件订阅者只能按照这种类型定义事件的处理方法

  • 二是定义与这个委托相关的event对象,使得 订阅者只负责注册和撤销自己的处理过程而不能随意对别人的处理过程产生影响

注意:

* 执行事件订阅列表中方法的顺序不能被保证

* 在这里采用的是同步调用方法,只有一个响应函数执行完毕,其它函数才会被执行。

* 如果要方法不被阻塞(包括这里的等待用户输入等),就需要采用异步调用方式。

Ref

https://blog.csdn.net/zzy7075/article/details/51322222 https://docs.microsoft.com/zh-cn/dotnet/api/system.multicastdelegate?view=netcore-3.1 https://docs.microsoft.com/zh-cn/dotnet/api/system.delegate?view=netcore-3.1 https://blog.csdn.net/u014224380/article/details/82996533 https://www.cnblogs.com/SkySoot/archive/2012/04/05/2433639.html https://www.cnblogs.com/SkySoot/archive/2012/04/05/2433639.html https://www.jianshu.com/p/027b199bfb8b