赞
踩
Covers:委托与事件
事件 在真实世界中无处不在,你可能不知道 C# 中事件的概念,但你一定了解这个词的本义。我们抛开编程不谈,先考察一下“事件”的逻辑性质。
你的朋友发来一条信息,约你去打球。关于这一事件,我们能总结出事件 3 个要素:
这个事件还隐含着两个未在字面上体现的要素:
小到水龙头中落下一滴水,大到洲际导弹发射、太阳熄灭、超新星爆发,任何事件要发生并有意义,至少要满足 5 个要素(当然还有更多,但至少是这 5 个):
C# 的事件机制就建立在这 5 个要素上,我们要学习的是将这五要素有效建模的一种“八股文”。
我们首先定义的是事件参数,因为它依赖的其它类最少,只要从 System.EventArgs 类派生一个就行了:
public class EVENT_ARGS : EventArgs {BODY}
这里定义了一个派生类 EVENT_ARGS,命名惯例是“事件种类+EventArgs”,如“ComputerBlowUpEventArgs”、“HeroSlayEventArgs”、“SunExtinguishEventArgs”(我怎么不想点好的);BODY 是其带有的字段,我们一般不在事件参数中定义方法。
隆重介绍 System.EventHandler,它是一个委托类型,接受一个任意 Object 和一个 EventArgs 类型的数据“包”。你可以猜到订阅关系是如何传播的 —— 利用委托的多播性质,当 EventHandler 被执行时,所有绑定到它的事件处理器就都被调用。
我们可以从自己定义一个“事件种类+EventHandler”,也可以直接使用原生的 System.EventHandler,自己定义的格式是:
public delegate void EVENT_HNDL(object sender, EVENT_ARGS e);
这里第一个参数名约定俗成为 sender
,第二个参数的类型是刚才定义的 EVENT_ARGS
,参数名约定为 e
。
事件源是一个类(不需要继承自某个系统类),它必须具有 3 个成员:
所以一个事件源类大概是这样的:
public class EVENT_SOURCE {
EVENT_HNDL _eventHandler;
public event EVENT_HNDL EVENT {
add {ADD_BODY}
remove {REMOVE_BODY}
}
void EVENT_EMIT(PARAMS) {BODY}
public void ENTRY...
}
上面的 event
关键字用于定义一个事件,其后紧接着的是 EVENT_HNDL 类名(这不是在声明字段或属性!只是一种约定的语法),然后是事件名 EVENT,最后是两个访问器组成的定义体,类似于属性的访问器,但使用了不同的关键字。
add
后接语句块定义该事件的 订阅器,用于将一个事件处理器绑定到事件委托对象 _eventHandler;remove
后接语句块定义该事件的 解除器,用于将一个事件处理器从事件委托对象中解除绑定。一般这样写:
public event EVENT_HNDL EVENT {
add {_eventHandler += value;}
remove {_eventHandler -= value;}
}
EVENT_EMIT 方法的编写、参数都是自由的,只要在其中有这么一句就行:
if (_eventHandler != null) {
_eventHandler(this, EVENT_ARGS_OBJ);
}
或其等效形式(这里的 ?.
其实就是 .
,只是多一层逻辑:如果委托为空绑定,则什么都不做):
_eventHandler?.Invoke(this, EVENT_ARGS_OBJ);
一般来说我们要把 EVENT_EMIT 方法写成私有的,这样做是出于安全考虑,不让不清楚情况的人随便直接 Invoke 造成混乱。因此我们一般还要再写一个暴露出来的方法 ENTRY,用来给别人调用。
订阅者也是一个类,它是事件的归宿,负责在事件到来时(前面的 _eventHandler 被 Invoke 时)做出一定的动作)。其定义是自由的,只要在其中包括一个事件处理器方法即可。
事件处理器是一个方法,必须满足 EVENT_HNDL 委托类型对函数签名的定义,比如用 System.EventHandler 时,其签名就应该是:
public void EVENT_PROC(object sender, EVENT_ARGS e)
事件处理器函数体根据功能需求写即可。
调用代码(比如 Main)中需要 初始化事件源实例(虽然你可以把事件源声明为静态的,但我们一般不这么做),如果订阅者类 EVENT_SUBSCRIBER 是非静态的,在调用处的代码中还需要初始化这个类的实例;如果是静态类则不需要;接着,我们需要为事件绑定处理器方法,事件是事件源的一个成员,它和委托一样可以用 +=
运算符;最后,我们调用一下事件源类暴露出的入口方法,开始等待事件发生即可。
综合一下写出来大概是这样的:
...
var source = new EVENT_SOURCE();
source.EVENT += EVENT_SUBSCRIBER.EVENT_PROC;
source.ENTRY;
现在五要素备齐,我们总结一下编写一个完整的事件发生-处理逻辑需要的关键环节:
看晕了不要紧,我们写一个例子来用上上面所有的八股套路,它实现的功能是每当用户按下键盘任意键(除 ESC)时,在控制台上打印一个感叹号:
using System; namespace LearnEvents { // Design-1. Define what message is carried when event is invoked: public class KeyPressEventArgs : EventArgs { public ConsoleKeyInfo Key { get; set; } } // Design-2. Define event handler delegate type: public delegate void KeyPressEventHandler(object sender, KeyPressEventArgs e); // Design-3. Define an event source: public class ConsoleSession { // Design-3a. Event handler delegate field: KeyPressEventHandler _keyPressEventHandler; // Design-3b. Define the event: public event KeyPressEventHandler KeyPress { add { _keyPressEventHandler += value; } remove { _keyPressEventHandler -= value; } } // Design-3c. Define a PRIVATE (for safety) invoker of the event handler: void OnKeyPress(KeyPressEventArgs args) { _keyPressEventHandler?.Invoke(this, args); } // Design-3d. Expose an "entry" method that is will call the invoker: public void Start() { var args = new KeyPressEventArgs(); for (;;) { args.Key = Console.ReadKey(true); // If ESC key is pressed, exit the session: if (args.Key.GetHashCode() == 27) return; OnKeyPress(args); } } } // Use-1. Define event subscriber, who has an event processor method: public static class ConsoleKeyListener { public static void KeyPressEventProcessor(object sender, KeyPressEventArgs e) { Console.Write("!"); } } // Use-2. Application code: class Program { static void Main() { // Use-2a. Initialize an event source object: var session = new ConsoleSession(); // Use-2b. Add a processor to the event handler: session.KeyPress += ConsoleKeyListener.KeyPressEventProcessor; // Use-2c. Finish, call the entry and wait for an event: session.Start(); } } }
实际编程中,事件源的事件定义部分几乎总是一样的,而委托字段又实在是没什么好写的。因此 C# 为我们提供了一个语法糖,我们可以只定义事件成员,不写访问器,也不写委托实例:
public event EVENT_HNDL EVENT;
上面的一行相当于隐式定义了一个委托字段,又定义了一个事件 EVENT,它具有默认的访问器。
用这种写法简写上例 design 的第三步:
... // Design-3. Define an event source: public class ConsoleSession { // [!!] Simplified: public event KeyPressEventHandler KeyPress; void OnKeyPress(KeyPressEventArgs args) { KeyPress?.Invoke(this, args); } public void Start() { var args = new KeyPressEventArgs(); for (;;) { args.Key = Console.ReadKey(true); if (args.Key.GetHashCode() == 27) return; OnKeyPress(args); } } } ...
进一步,我们还可以直接使用 System.EventHandler 简化 design 阶段。
上面的例子使用默认 EventHandler 简写:
... class KeyPressEventArgs : EventArgs { public ConsoleKeyInfo Key { get; set; } } class ConsoleSession { public event EventHandler KeyPress; void OnKeyPress(EventArgs args) { KeyPress?.Invoke(this, args); } public void Start() { var args = new KeyPressEventArgs(); for (;;) { args.Key = Console.ReadKey(true); if (args.Key.GetHashCode() == 27) return; OnKeyPress(args); } } } public static class ConsoleKeyListener { public static void KeyPressEventProcessor(object sender, EventArgs e) { Console.Write("!"); } } ...
你可能会问,我们定义了 KeyPressEventArgs,却在事件处理器中将其作为 EventArgs 接受,要怎么再拿出其中的时间参数呢?
还记得我们之前学过的拆箱吗:
...
public static void KeyPressEventProcessor(object sender, EventArgs e)
{
var key = e as KeyPressEventArgs;
Console.WriteLine(key?.Key.GetHashCode());
}
...
这里我们将 e 拆箱,“升级”成 KeyPressEventArgs 对象,由于 KeyPressEventArgs 派生自 EventArgs,且我们能够确定传入的 e 一定是 KeyPressEventArgs 对象在 OnKeyPress 被调用时装箱而成的,所以这个操作不会抛出异常;我们在访问 key 之前还对其进行了“防 null 检查”。
至此我们已经掌握了 C# 事件的大部分必要知识,也是整部笔记中最难的知识。
其实在实际开发中,我们更多的是用这些知识来调用平台已经为我们写好的事件,我们要做的只是编写合适的事件处理器,并绑定到事件上(相当于平台的开发者已经为我们完成了 design 阶段,我们只需要做 use 阶段的事情)。
我们程序员经常与“黑窗户”控制台打交道,但游戏是一个图形界面。图形界面是复杂的,没办法保证用户总是“先点击这个,再点击那个”,因此像控制台一样做“流程式”交互的思路是行不通的;我们必须定义“如果用户点这个,我们怎样响应”,再把实际的交互工作交给 .NET 中生生不息的“事件泵”去完成,我们称之为 事件驱动 的程序开发理念。
鉴于有的朋友还压根儿没下载好 Unity,只是来打语言基础的,我们在这里不涉及具体的 Unity API。但我想写一个小例子来简单体验一下事件驱动编程,我对 Winform 开发不感兴趣,因此也不会太过认真地去写了。
首先我们添加对 Windows.Forms 的依赖,Rider 中是这样操作的:
接着搜索“Fonts”,勾选并添加 System.Windows.Forms 依赖:
然后敲代码:
using System.Windows.Forms; namespace LearnEvents { class ClickMe : Form { Label _lbl; Button _btn; int _remainingClicks; public ClickMe() { _lbl = new Label(); _btn = new Button(); Controls.Add(_lbl); Controls.Add(_btn); Text = "Click Game"; Height = 180; Width = 250; FormBorderStyle = FormBorderStyle.FixedSingle; _lbl.Top = 40; _lbl.Left = 75; _lbl.Height = 20; _lbl.Width = 100; _lbl.Text = "↓ Click it!"; _btn.Top = 60; _btn.Left = 75; _btn.Height = 50; _btn.Text = "No, don't!"; _remainingClicks = 10; _btn.Click += (sender, e) => { --_remainingClicks; if (_remainingClicks <= 0) { _btn.Enabled = false; _btn.Text = $"I'm dead"; _lbl.Text = " Good job!!"; } else { _btn.Text = $"Remaining clicks: {_remainingClicks}"; } }; } } class Program { static void Main() { var form = new ClickMe(); form.ShowDialog(); } } }
这是一个无聊的点按钮小程序:
本节就先到这吧。
T.B.C.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。