前言
因为接下来想要整理一下游戏里面的事件模块,而当前项目的业务开发都是通过 Lua 进行的,有一阵子没有写 C#了,回想 C#的委托和事件,有些细节一时模糊了起来,所以打算干脆把相关的知识点整理一下,就当做复习了。
委托
什么是委托
无论是委托还是事件官网都没有一个简单直白的定义,只能说理解万岁。
委托:用于封装方法的一种引用类型,类似于 C++的指针,但其类型安全可靠。通过封装多个方法到一个变量中,调用此变量就可以调用所有绑定的方法。你也可以将它理解性地看做“方法的容器”。
委托的写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
public delegate void MyEventHandler();
public MyEventHandler MyEvent;
public delegate void MyEventHandler<in T1, in T2>(T1 arg1, T2 arg2); public MyEventHandler<T1, T2> MyEvent;
public delegate TResult MyEventHandler<in T1, out TResult>(T1 arg); public MyEventHandler<T1, TResult> MyEvent;
public Action MyEventHandler; public Action<in T> MyEventHandler;
public Func<int> MyEventHandler; public Func<in T, out TResult> MyEventHandler;
|
委托的作用
熟练使用委托可以提高代码的拓展性,优化代码结构。
这种拓展性的应用场景主要体现在于两个方面:
- 代码结构不变的情况,执行处需要在不同的环境下给委托赋值不同的方法,就可以执行不同的功能。
- 一个功能的触发,涉及到多个方法的执行。
先说第一种应用,这里以 GameFramework 框架中的写法举例,在 GameFramework 框架中(后面成为 GF)可以分为 GF 层和 UGF 层,GF 层的逻辑是功能的核心逻辑,而 UGF 层则是 GF 在 Unity 的应用。在 GF 的 UI 模块,一个页面的打开/关闭的完成正如其他 UI 框架很可能执行的逻辑一样,通过事件系统派发出了一个 UI 页面打开/关闭的事件。而为了保证 GF 层的结构和功能不变动,提供框架使用者可以根据情况自由拓展或者改变这样的操作。GF 层打开/关闭页面的 Internal 逻辑执行完成后,并没有直接调用事件模块的接口,而是执行了一个委托。 在 UGF 层,使用者就可以通过绑定自己的方法到这个委托上,来实现自己的页面打开/关闭后的处理逻辑,而不会完全不需要接触和改动 GF 层的代码。让我们直接看一下代码写法,
GF层的UIManager:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
|
private EventHandler<OpenUIFormSuccessEventArgs> m_OpenUIFormSuccessEventHandler; private EventHandler<OpenUIFormFailureEventArgs> m_OpenUIFormFailureEventHandler;
public event EventHandler<OpenUIFormSuccessEventArgs> OpenUIFormSuccess { add { m_OpenUIFormSuccessEventHandler += value; } remove { m_OpenUIFormSuccessEventHandler -= value; } }
public event EventHandler<OpenUIFormFailureEventArgs> OpenUIFormFailure { add { m_OpenUIFormFailureEventHandler += value; } remove { m_OpenUIFormFailureEventHandler -= value; } }
private void InternalOpenUIForm() {
if (m_OpenUIFormSuccessEventHandler != null) { OpenUIFormSuccessEventArgs openUIFormSuccessEventArgs = OpenUIFormSuccessEventArgs.Create(uiForm, duration, userData); m_OpenUIFormSuccessEventHandler(this, openUIFormSuccessEventArgs); ReferencePool.Release(openUIFormSuccessEventArgs); } }
public void CloseUIForm() {
if (m_CloseUIFormCompleteEventHandler != null) { CloseUIFormCompleteEventArgs closeUIFormCompleteEventArgs = CloseUIFormCompleteEventArgs.Create(uiForm.SerialId, uiForm.UIFormAssetName, uiGroup, userData); m_CloseUIFormCompleteEventHandler(this, closeUIFormCompleteEventArgs); ReferencePool.Release(closeUIFormCompleteEventArgs); }
}
|
UGF层,UIComponent中的使用形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
protected override void Awake() { base.Awake(); m_UIManager = GameFrameworkEntry.GetModule<IUIManager>(); if (m_UIManager == null) { Log.Fatal("UI manager is invalid."); return; } if (m_EnableOpenUIFormSuccessEvent) { m_UIManager.OpenUIFormSuccess += OnOpenUIFormSuccess; }
m_UIManager.OpenUIFormFailure += OnOpenUIFormFailure; }
private void OnOpenUIFormSuccess(object sender, GameFramework.UI.OpenUIFormSuccessEventArgs e) { m_EventComponent.Fire(this, OpenUIFormSuccessEventArgs.Create(e)); }
private void OnOpenUIFormFailure(object sender, GameFramework.UI.OpenUIFormFailureEventArgs e) { Log.Warning("Open UI form failure, asset name '{0}', UI group name '{1}', pause covered UI form '{2}', error message '{3}'.", e.UIFormAssetName, e.UIGroupName, e.PauseCoveredUIForm.ToString(), e.ErrorMessage); if (m_EnableOpenUIFormFailureEvent) { m_EventComponent.Fire(this, OpenUIFormFailureEventArgs.Create(e)); } }
|
再说第二个应用,通过向委托里面添加多个方法,可以实现一个功能的触发,涉及到多个方法的执行。这个从应用上来说其实很好理解,比如游戏中,我使用了恢复血量的药瓶这个道具,那么我的生命值就会进行增加,以及我的敌人会发现我在喝药,并立马进行攻击(bushi)。这种情况其实就是利用一个功能的触发,涉及到多个方法的执行这样的一个原理做到的,这样的功能实现也是建立在委托的支持上的。
委托的缺点
封装的矛盾。面向对象设计,讲究的是对象的封装。委托可以很好把方法进行封装,这是毋庸置疑的,但是委托同时还可以被外部直接进行赋值,一旦赋值就会失去原来绑定好的内容,这严重破坏了委托的封装性。
而如果把委托设置为Private的话,又会让其完全失去原来的作用。这也是上面GF里面之所以声明委托为Private,并通过事件进行封装对外赋值接口的设计原因。
这种情况,就要引入事件这个概念了。
事件
事件是建立在对委托的语言支持之上的。通过给委托变量加上限制关键词 “event”,就可以声明一个事件。可以理解为声明一个事件不过类似于声明一个进行了封装的委托类型的变量而已。
1 2 3
| public delegate void MyEventHandler(object sender, MyEventArgs e);
public event MyEventHandler MyEvent;
|
这样声明的事件,相较于普通的委托变量,有如下几个限制:
- 其赋值只能在事件发布者内部进行,外部不能直接赋值。
- 其执行只能在事件发布者内部进行,外部不能直接调用。
通过这种事件的形式,我们可以获取比委托更好的封装性。
利用委托的第二个应用,配合上事件,达成的类似喝血瓶的例子这种模式,进行抽象后就是所谓的 “观察者模式”,它是为了定义对象间的一种一对多的依赖关系,以便于当一个对象的状态改变时,其他依赖于它的对象会被自动告知并更新,是一种 松耦合 的设计模式。
.NET框架中的委托与事件
.NET Framework中有着固定的规范:
- 命名空间System下存在委托原型定义
1
| public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
|
- 委托类型和变量的名称和都应该以EventHandler结束
- 事件的命名为委托去掉EventHandler之后剩余的部分
- 继承自 EventArgs 的类型应该以EventArgs 结尾
看到这个原型定义,可以注意到传参的两个点,一个是传参包含了object sender,另一个是 EventArgs 的类型。sender传递的是事件的发布者,EventArgs 则是事件的附加信息,包含监视者需要用到的信息。
EventArgs是存在System命名空间下的一个类,通过继承EventArgs类,可以定义并写入任何自己想要的数据传递给监视者,这一点从上面GF的实例中体现出来的同时,还可以看到对于传递的EventArgs类型,它还拓展进了引用池来保证回收利用。
参考文档