Unity 中的委托与事件
前言
本篇博客是为了给下一篇博客:UE 中的委托机制,铺路的。起因是在学习 ue 的过程中发现 ue 使用委托这个概念的频率与深度都比 unity 中要高得多,因此在这里总结一下学习 unity 期间对于委托的理解。
C# 中的委托与事件
C# 委托 ( delegate )
C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。
委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。
- 声明委托
委托声明决定了可由该委托引用的方法。委托可指向一个与其具有相同标签的方法。
例如,假设有一个委托:
1 | public delegate int MyDelegate (string s); |
上面的委托可被用于引用任何一个带有一个单一的 string
参数的方法,并返回一个 int
类型变量。
声明委托的语法如下:
1 | delegate <return type> <delegate-name> <parameter list> |
- 实例化委托
一旦声明了委托类型,委托对象必须使用 new 关键字来创建,且与一个特定的方法有关。当创建委托时,传递到 new 语句的参数就像方法调用一样书写,但是不带有参数。例如:
1 | public delegate void printString(string s); |
- 委托的多播
委托对象可使用 “+” 运算符进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可被合并。“-” 运算符可用于从合并的委托中移除组件委托。
使用委托的这个有用的特点,您可以创建一个委托被调用时要调用的方法的调用列表。这被称为委托的 多播(multicasting),也叫组播。
为什么 C# 会有委托?
解决观察者模式的缺陷。这里又引申出一个问题,什么是观察者模式,观察者模式的缺陷又是什么?在回答这个问题前,我们其实已经知道一个答案,那就是先有设计模式中的观察者模式,再有委托事件这一技术。它有一个前后顺序,如果你要彻底学会委托,那么就要先学会观察者模式,还要了解观察者模式的缺陷。
观察者模式的缺陷
观察者模式是对主题对象和观察者对象进行解耦,使双方都依赖与抽象,而不是依赖于对方的具体对象,使双方的变化都不会影响到对方的具体对象。当多个对象需要根据一个对象的状态发生相应的改变或操作时,可使用观察者模式。
这里我写一个小 demo,非常简洁。还是猫和老鼠,猫是主题对象,人和老鼠是观察者。猫叫的时候,人和老鼠要执行相应操作。
使用观察者模式
1 | using System.Collections; |
使用委托
1 | using System; |
对比
做一下对比可以发现:
1.实现同样的功能,使用委托非常的简洁,并且主题猫的类里没有任何观察者类成员,两者是完全独立的。而观察者模式双方并没有完全的独立,抽象主题通知时依然依赖于抽象观察者。
2.观察者不需要继承观察者接口,它的通知方法名可以不同,比如说人的执行方法是 Notify()
,猫的执行方法是 Run()
,现实中本就是这样的,方法名本就不一定相同。
也就是说观察者模式的缺陷为:
1.抽象主题依旧依赖于抽象观察者。
2.具体的观察者,通知方法被固定了。
观察者模式存在的意义就是解耦,它使观察者和被观察者的逻辑不再搅在一起。而是彼此独立,互不依赖。而使用委托则能使得观察者和主题完全解耦,甚至不需要知道对方的存在。
事件 ( event )
一言以蔽之,事件就是被 event
关键字修饰过的委托类型。对委托的例子做一个小小的变动:
1 | public class DeCat |
没错我们给声明前面加个 event
就可以了,但是你突然发现,编译不过去,有报错。这也是事件与委托核心的区别。
事件是类或对象向其他类或对象通知发生的一种特殊签名的委托。它的本意是对委托的封装,它在外部只能被订阅或取消订阅,但是不能发布。一句话总结:事件只能在外部被订阅,但是不能在外部被触发。只能通过内部的公开方法,在方法内部触发事件,这样可以使得程序更加的安全。
这里我通过的例子来说明:当使用委托变量时,客户端可以直接通过委托变量触发事件,也就是直接调用 deCat.call()
;这将会影响到所有注册了该委托的变量,而事件的本意应该为在事件发布者在其本身的某个行为中触发。通过添加 event
关键字来发布事件,事件发布者的封装性会更好,事件仅仅是供其他类型订阅,而客户端不能直接触发事件(语句 deCat.call()
无法通过编译),事件只能通过事件发布者 DeCat
类的内部触发(比如在方法 deCat.CallMethod()
中)。换言之,就是 call()
语句只能在 deCat
内部被调用。这样才是事件的本意,事件发布者的封装才会更好。
Action & Func & Predicate
写完 STL 现在看到 predicate 就 PTSD 了有木有。
C# 内置了几种泛型的委托,使用起来非常方便,分别是 Action
、Func
与 Predicate
。
Action
Action
是无返回值的泛型委托,通过函数重载提供了 0 ~ 16
个参数不同类型的 Action
。
- Action 的定义
1 | public delegate void Action(); // 无参数 |
- Action 的使用举例
1 | // 使用举例 |
Func
Func
是有返回值的泛型委托,通过函数重载提供了 0 ~ 16
个参数不同类型的 Func
。
- Func 的定义
1 | public delegate TResult Func<out TResult>(); // 无参数 |
- Func 的使用举例
1 | // 使用举例 |
Predicate
Func
是返回值类型为 bool
型的泛型委托,只能传递一个参数,因此没有其他的重载。
1 | // 只有一个泛型委托,没有多参数重载 |