C# 委托与事件
委托
委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联。
声明委托
类似于 class
或 struct
,委托定义创建了一个新的类型。C# 的类型系统要求所有类型定义(包括类、结构体、枚举和委托)都必须在类型作用域(如类、接口、命名空间)中进行,不能在方法中定义。
1 | delegate <return type> <delegate-name> <parameter list>; |
提示
在方法重载的上下文中,方法的签名不包括返回值。 但在委托的上下文中,签名包括返回值。 换句话说,方法和委托必须具有相同的返回类型。
创建委托实例
使用定义的委托类型来创建委托实例,并将其绑定到符合签名的方法。
1 | public delegate void MyDelegate(string message); |
调用委托
1 | // 上个代码块定义的三个 MyDelegate委托 |
当委托具有返回值时,直接使用委托实例()
方式来调用委托时,Visual Studio 并不会提示其含有返回值,但是能正常获取到返回值,使用.Invoke()
来调用委托时,Visual Studio会提示其返回值类型。
1 | public delegate string MyDelegate2(string s); |
官方文档备注
公共语言运行时为每个委托类型提供 Invoke
方法,其签名与委托相同。 无需从 C#、Visual Basic 或 Visual C++显式调用此方法,因为编译器会自动调用此方法。
委托的static
委托的声明不能被static
修饰符修饰,委托是用来定义方法签名的类型,不涉及对象的实例化,因此static
关键字在委托类型的声明上是没有意义的。参考附录中的类型定义
但是委托的实例可以使用static
修饰,并且委托的非静态实例也可绑定静态方法。
1 | public delegate void MyDelegate(); |
委托的多播
委托不仅可以引用单个方法,还可以引用多个方法,这种能力被称为多播委托。多播委托允许将多个方法添加到同一个委托实例中,并在调用该委托时依次调用这些方法。
+=
运算符可以将多个方法添加到一个委托实例中。
-=
运算符可以从委托中移除特定的方法。
当调用多播委托时,委托中的方法按照添加的顺序被依次调用。
如果多播委托含有返回值,则只有最后一个被调用的方法的返回值会被传递给调用者。前面的方法的返回值将不会被返回给调用者。
下面是多播委托的示例:
1 | public delegate string MyDelegate(string message); |
在定义一个委托时,它会继承System.MulticastDelegate
(多播委托类),而MulticastDelegate
又继承自Delegate
,并且这两个类是密封的。
我们通过Visual Studio查看Delegate
与MulticastDelegate
会发现它们两个都是抽象类,并且没有密封关键字sealed
,但的确不能从其派生出自定义类。
在官方文档中是这样描述的
Delegate 类是委托类型的基类。 但是,只有系统和编译器才能从 Delegate 类或 MulticastDelegate 类显式派生。 不允许从委托类型派生新类型。 Delegate 类不被视为委托类型;它是用于派生委托类型的类。
大多数语言实现
delegate
关键字,并且这些语言的编译器可以从 MulticastDelegate 类派生;因此,用户应使用语言提供的delegate
关键字。
多播原理
多播委托内部维护了一个方法调用列表,当调用多播委托时,所有的方法都会按照顺序依次被调用。
委托的多播实现方式虽然类似于链表结构,但实际上并不严格是链表,只能说委托内部的调用列表更接近于一个符合链表定义的数据结构。
为什么说符合链表的定义?
- 调用列表:委托的调用列表内部包含了一组方法的引用。当你使用
+=
或-=
操作符来添加或移除方法时,委托会创建一个新的委托实例,其中调用列表会被更新。这种行为类似于链表的“增删节点”操作,但并非严格意义上的链表。 - 引用关系:每个委托实例可能持有对其他委托实例的引用,这种引用关系在逻辑上表现为链表结构。即,委托链中的每个委托对象都指向下一个委托对象。
不严格是链表的原因:
- 实现细节:实际的委托实现中,调用列表可能被存储在数组、列表或其他数据结构中,而不是严格的链表结构。在
System.MulticastDelegate
中我们可以发现,其私有属性_invocationList
(调用列表)是object
类型,在各个方法中,几乎都被转换为object[]
类型去使用。 - 封装:C# 和 .NET 框架对委托的内部实现进行了高度封装,开发者无法直接访问这些内部数据结构。因此,开发者所能看到的只是委托作为一个整体对象的行为,而无法直接操作其内部的调用列表。
官方文档备注
回调函数
在之前的学习中,我们可以发现委托经常作为回调函数,比如Array.Sort()
与Array.Find()
等。
1 | int[] arr = new int[] { 4, 3, 2, 5, 1, 24, 2, 1, 35 }; |
常见委托
- Action:无返回值的委托,用于封装不带返回值的方法,可以带有 0 ~ 16个参数
- Func:有返回值的委托,用于封装带返回值的方法。
Func
最多可以有16个参数,最后一个泛型参数表示返回值类型。 - Predicate:
Predicate
是一种特殊类型的Func
,它总是返回bool
,用于封装带有一个参数的条件检查方法。 - Comparison:
Comparison
是一种用于比较两个同类型对象的委托,常用于排序方法中。该委托返回一个整数,指示第一个对象是否小于、等于或大于第二个对象。 - Converter:
Converter
是一种用于将一种类型的对象转换为另一种类型的委托。它接收一个类型TInput
的参数并返回类型TOutput
的结果。 - EventHandler:
EventHandler
是一种专门用于处理事件的委托类型。它不带返回值,通常用于事件处理程序。
1 | // 常见委托: Action,Action<T>,Func<T>,Predicate<T>,Converter<T1,T2>,EventHandler |
协变与逆变
向委托分配方法时,协变和逆变为匹配委托类型和方法签名提供了灵活性。 协变允许方法具有的派生返回类型多于委托中定义的类型。 逆变允许方法具有的派生参数类型少于委托类型中的类型。
协变作用于返回值上,逆变作用于参数上
协变:使用派生类返回值替代基类返回值
逆变:使用基类参数替代派生类参数
协变
1 | class Mammals { } |
上面的代码中,public delegate Mammals HandlerMethod();
这个委托的返回值类型设置的是Mammals
类,通过协变,可以将返回值为Dogs
类的方法绑定到该委托上。
逆变
下面这两个委托的参数KeyEventArgs
与MouseEventArgs
都是EventArgs
的派生类
1 | public delegate void KeyEventHandler(object sender, KeyEventArgs e); |
1 | public Form1() |
事件
事件是委托的实例,是一种特殊的委托,仅可以从声明事件的类(或派生类)或结构(发布服务器类)中对其进行调用,在GUI
编程中,用于响应用户的操作,如鼠标点击,键盘输入等。
事件是类的成员,与字段,属性,方法具有相同的成员级别,因此事件无法在方法中定义,只能在类中定义。
如何理解事件是委托的实例,但委托的实例不一定是事件?
委托定义了方法的签名,类似于类定义了对象的结构,委托的实例可以作为类的成员,也可以在方法中实例化使用,类似于对象的实例化和使用。而事件在这里,是作为类的成员的委托实例,但委托的实例也可以在方法中实例化使用,因此委托的实例不一定是事件。
为什么说事件是特殊的委托
事件基于委托实现,但与普通委托相比,它有一些特殊的限制和功能。
事件只能在声明它的类内部触发,而不能在外部类或对象中触发,保证了时间的触发控制权仅限于事件的发布者。
事件只能在声明它的类内通过
=
赋值,进而替换整个处理程序列表。在外部,仅能使用+=
或-=
操作符来订阅或取消订阅事件处理程序,不能使用=
操作符替换整个处理程序列表。
事件的声明
事件由发布者触发,订阅者通过事件处理程序来响应事件。
1 | public class Publisher |
事件的订阅与取消
- 订阅:订阅者通过
+=
操作符将自己的方法附加到事件上,以便在事件触发时得到通知。 - 取消订阅:通过
-=
操作符,可以将方法从事件中移除,以取消订阅。
示例:
1 | public class Publisher |
观察者模式
观察者模式:
- 定义:在观察者模式中,一个对象(被观察者)维护一组依赖它的对象(观察者)。当被观察者的状态改变时,它会自动通知所有观察者,以便它们能够做出相应的反应。
- 结构:通常包括一个被观察者(Subject)和一个或多个观察者(Observers)。被观察者提供注册、移除和通知观察者的方法。
事件在C#中的实现:
- 发布者(Publisher):对应观察者模式中的被观察者。
- 订阅者(Subscriber):对应观察者模式中的观察者。
- 当发布者的状态改变时,通过触发事件来通知所有已注册的订阅者(即观察者),这些订阅者随后会调用它们的事件处理程序以响应这个状态变化。
上面事件的订阅与取消中的示例的代码块中,展示的便是观察者模式。
发布-订阅模式
发布-订阅模式是一种消息传递模式,通常通过消息代理(Message Broker)来解耦发布者和订阅者。在发布-订阅模式中,发布者发布消息,而订阅者接收消息,双方并不直接交互。
1 | using System; |
观察者模式与发布-订阅模式的差异
- 中介/消息代理:在发布-订阅模式的例子中,
MessageBroker
类扮演了中介的角色,它在发布者和订阅者之间传递消息。发布者和订阅者通过MessageBroker
进行通信,而不是直接相互引用。这种设计使得发布者和订阅者更加解耦。 - 直接引用:在观察者模式的例子中,
Publisher
直接与观察者(Subscriber
)交互。观察者通过订阅Publisher
的事件来获取通知,没有中介存在。Publisher
类是具体的发布者,Subscriber
类是具体的观察者。 - 解耦:发布-订阅模式通过中介进一步解耦了发布者和订阅者,彼此不知道对方的存在。观察者模式中,发布者和观察者之间仍然存在一定的耦合,因为观察者直接订阅了发布者的事件。
发布-订阅模式更适用于需要高度解耦的场景,通过中介或消息代理来隔离发布者和订阅者。
观察者模式则直接将观察者与被观察者绑定,更适合用于单一对象之间的依赖关系管理。
事件的高级用法
- 事件的自定义添加器和移除器:你可以通过
add
和remove
关键字自定义事件的订阅和取消订阅行为。 - 事件代理:可以在一个对象中订阅另一个对象的事件,并将事件转发给其他对象,形成事件链。
示例:
1 | public class Publisher |
附录
类型定义
类型定义是在程序中创建新的数据类型。例如,下面的 Person
类和 MyDelegate
委托就是类型定义。它们定义了一种新的数据类型,可以用来创建对象或委托实例。
1 | // 定义一个类(自定义类型) |
在 C# 中,“类型定义”指的是创建一个新的数据类型,比如类(class
)、结构体(struct
)、枚举(enum
)、接口(interface
)以及委托(delegate
)。这些类型的定义在编译时为程序引入了新的类型标识符,它们可以在整个程序中被引用和使用。
类型定义中,除了class
,其他类型定义(结构体,枚举,接口,委托)不支持static
修饰符作为类型定义的一部分,类与(c#8.0之后版本的)接口内部的成员可以是静态的。
类型定义 vs. 变量声明
- 类型定义:指的是引入一个新的类型,例如
class MyClass
、struct MyStruct
、enum MyEnum
或delegate void MyDelegate()
。这是一个编译时的概念,类型定义只能在全局作用域或类、结构体、接口等作用域中进行。 - 变量声明:指的是在程序中声明一个变量,并指定它的类型。例如,
int myNumber = 5;
是一个变量声明,int
是一个预定义的类型,而myNumber
是该类型的一个实例(即变量)。变量声明可以在方法的局部作用域中进行。
成员级别
成员级别是指在类中定义的元素(如字段,属性,方法)在类的层级结构中的位置或作用域,这些元素被称为类的成员,它们共享同一个作用域,并且在类的整个范围内都可以访问。
成员类型
在一个类中,成员通常包括以下几种类型:
- 字段(Fields):用于存储对象的状态或数据。
- 属性(Properties):提供对字段的访问,通常通过 getter 和 setter 控制读写权限。
- 方法(Methods):定义类的行为,封装具体的功能或操作。
- 事件(Events):用于发布和订阅通知,支持观察者模式。
- 构造函数(Constructors):用于初始化对象实例。
- 索引器(Indexers):允许对象使用数组语法进行访问
这些成员都是类的组成部分,它们共享类的作用域,并且可以被类的实例或类的其他成员访问。
成员级别与方法级别
成员级别:指的是类内部直接定义的成员。它们的作用范围是整个类,这意味着它们在类的所有方法中都可以被访问和使用。例如,类中的字段和方法就是成员级别的元素。
方法级别:指的是在方法内部定义的局部变量或局部函数。它们的作用范围仅限于方法内部,方法之外的代码无法访问这些元素。
类型级别
与“成员级别”不同,类型级别更关注类、结构、接口、枚举等类型本身的定义和作用范围,而不是这些类型内部的成员。
类型级别指的是类型本身的定义和其在整个程序中的可见性与作用范围。类型级别主要讨论的是类型在命名空间、程序集(Assembly)或整个程序中的位置和可见性。通常涉及的类型包括类(Class)、接口(Interface)、结构(Struct)、枚举(Enum)、委托(Delegate)等。
而类型的可见性与作用范围一般由访问修饰符来控制。
参考内容
- 标题: C# 委托与事件
- 作者: 日之朝矣
- 创建于 : 2024-08-17 16:05:04
- 更新于 : 2024-08-18 09:25:27
- 链接: https://blog.rzzy.fun/2024/08/17/csharp-delegate-and-event/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。