一、委托基础
- 委托类型
- 在 C# 里能写出的所有委托基本上都是多播委托(
MulticastDelegate),常规方式无法写出普通的委托(Delegate),底层实现中才可能出现Delegate类型。 - 多播委托可以添加多个方法,使用
+=运算符添加方法,其内部的invocationList用于存储方法。当委托为普通委托时,
invocationList指向一个方法;为多播委托时,它是一个数组,可存储多个方法。 - 可通过
GetInvocationList方法获取invocationList中的所有委托,该方法返回一个Delegate数组。
- 在 C# 里能写出的所有委托基本上都是多播委托(
- 委托特性及问题
委托内部存储了一个方法列表(数组),因此委托存在一些问题:-
调用报错中断:调用委托时,如果其中一个委托报错,后面的委托不会被调用。
-
返回值问题:如果委托有返回值,注册多个方法时,调用委托(
Invoke)的返回值会是最后一个方法的返回值。 -
移除方法时间复杂度:使用
-=运算符移除方法时,时间复杂度是 O(n)。从委托方法列表中Remove一个方法时,对该方法的查找是从后往前进行的
若移除最早添加的方法,查找时间会显著增加。
-
线程不安全:对委托数组的添加或删除操作没有提供线程锁,线程不安全。
事件(对委托的一种封装)是线程安全的
-
- 委托与函数指针的区别
- 委托可以指向多个函数:委托可以指向多个函数,而函数指针做不到。
- 委托可以指向同一函数多次:委托可以指向同一个函数多次,在调用时会按顺序多次调用该函数,函数指针无法实现。
- 包含对象信息:在 C# 中,将方法添加到委托时,方法所在类的实例信息也会作为信息传递过去,委托的函数引用包含了方法所在对象的信息;而 C++ 中的函数指针只是函数的入口地址,不具备这些信息。
- 委托的异步调用(几乎已弃用)
- 委托提供了
BeginInvoke和EndInvoke方法用于异步调用,但在 .NET Core 及后续版本中不再使用。 - 在旧版本中,
BeginInvoke会在另一个线程上运行任务,返回一个IAsyncResult类型,可通过其IsCompleted属性判断任务是否完成,使用EndInvoke结束调用并获取结果。现在通常使用async/await和Task进行异步编程。
在 UI 开发(如 WinForm、WPF)中,控件的
Dispatcher有BeginInvoke方法,但一般采用开多线程或异步任务,完成任务后回到 UI 线程操作控件或调用Dispatcher.Invoke。 - 委托提供了
二、逆变与协变
- 概念
- 协变(Covariance):把一个派生类转换为它的父类,通常可使用隐式转换。在委托中,若委托返回值类型为父类(如
object),可以添加返回值为派生类(如string)的方法,这是允许的,即从派生类转为父类。 - 逆变(Contravariance):把一个父类对象转换为它的派生类,通常使用显式转换。在委托中,若委托参数类型为派生类(如
string),可以添加参数类型为父类(如object)的方法,相当于把父类逆变成派生类。
.NET 中原生的强类型委托除了
Action和Func外,还包含一个Predicate(该委托并不常用),其返回值一定是布尔值,参数只能有一个int,常用于筛选,但在实际中,返回值为布尔值且参数为一个的lambda表达式通常会被转换为Func而非Predicate。 - 协变(Covariance):把一个派生类转换为它的父类,通常可使用隐式转换。在委托中,若委托返回值类型为父类(如
- 委托的逆变与协变:委托的逆变与协变指的是其传参以及返回值,泛型委托还提供了泛型逆变与协变,如
Action中的in表示参数协变,IEnumerable中的out表示参数逆变。

三、委托与接口的比较
如果委托是为了实现将函数作为参数进行传参的话,为什么不使用接口呢?
使用接口的缺点:
- 不灵活:使用接口传递不同形式参数时,需要定义多种接口,且必须有类去实现接口,繁琐且不实用。
比如使用LINQ时,要频繁传入各种形式的函数
- 暴露公共成员:传递接口对象实际上是传递类的实例引用,可能会暴露类中的其他公共成员,引发不必要麻烦。
- 无法使用多播功能:接口无法像委托那样提供添加和删除等多播功能。
- 方法封装固定:类的方法封装好后难以定制更改,而匿名委托(如
lambda表达式)可随时声明,能使用当前作用域下的变量,提供高度灵活性。 - 内置强类型委托丰富:C# 内置了很多强类型委托,如
Action、Func、Predicate等,以及处理标准事件的EventHandler,无需编写大量接口来满足不同参数和返回值需求。
四、事件
- 事件的本质
- 事件使用
event加上委托类型和事件实例名称声明,实际上是一个语法糖:- 事件将委托以私有变量的形式封装在类内,外部无法直接访问和调用
Invoke方法。 - 事件对委托进行了封装,定义了
add和remove两个方法,类似于属性的get和set,用于合并和移除委托。 - 事件提供了线程安全性,在
add与remove中使用Interlocked.CompareExchange进行线程锁操作。
- 事件将委托以私有变量的形式封装在类内,外部无法直接访问和调用
- 事件使用
- 自定义事件
-
可以像自定义属性一样自定义事件的
add和remove方法。例如使用List<Action>存储委托,add方法调用List的Add方法添加委托,remove方法调用List的Remove方法移除委托,还可提供公共方法调用委托列表中的每个委托。但自定义时要注意多线程安全性问题,若不提供线程安全措施,在多线程环境下频繁进行add或remove操作可能会出现问题。 -
在某些情况下,如 WPF 中使用 MVVM 模式实现
RelayCommand类时,需要对事件的add和remove方法进行定制化,可使用CommandManager的静态事件RequerySuggested对传入的函数进行注册或删除。也可以进行一些特殊定制,但不具有实际意义,只是为了展示其可行性。

运行结果为
3 method registered
-
7331

被折叠的 条评论
为什么被折叠?



