应该记住的事件
本页内容
回顾 | |
代理和事件 | |
代理 | |
事件 | |
网站收集袋 |
回顾
上个月,我们讨论了性能和装箱。有读者提出了这样的问题:Perl 版本为何比 C# 版本快这么多。答案是,Perl 具有这方面的专长。一位读者指出,我忘记告诉 Regex 应编译正则表达式,而不是逐对进行解释。在 Beta 1 中,这是通过将“c”当作第二个参数传递给 Regex 构造器来实现的(在 Beta 2 中有一个枚举具有同样的功能)。它可将开销降低近一半,并可将最快版本的经历时间降低到 7 秒以下。
代理和事件
本月专栏的主题是代理和事件。确切地说是代理,因为事件将在下个月进行讨论。我首先要解释代理的定义、功能和使用方法,接着我们将在下个月对事件进行讨论。
代理
在大多数情况下,当调用函数时,我们会指定要直接调用的函数。如果类 MyClass 具有一个名为 Process 的函数,我们通常会按如下方法进行调用:
MyClass myClass = new MyClass(); myClass.Process();
这样调用在大多数情况下都是可行的。但是有些时候,我们不想直接调用函数,而希望能够将它传递给其他人,让他们能够进行调用。在以事件驱动的系统(如图形用户界面)中,这种方法尤为有用。例如当我需要在用户单击某个按钮即可执行一些代码时,或者当我要记录一些信息但却无法指定记录方式时。
考虑以下示例:
public class MyClass { public void Process() { Console.WriteLine("Process() begin"); // 这里还有其他东西... Console.WriteLine("Process() end"); } }
在此类中,我们进行一些记录,以了解函数的开始时间和结束时间。但是,我们的记录仅限于发送到控制台,这可能不是我们所需要的。我们真正需要的是能够控制从函数外部记录信息的位置,同时不必使函数代码变得复杂。
在这种情况下,代理便是理想的解决方法。代理使我们可以指定将要调用的函数“类似于什么”,而不必指定调用“哪一个”函数。对代理的声明类似于对函数的声明,不同的是在这种情况下,我们所声明的是此代理可引用的函数签名。
对于以上示例,我们将声明一个带有单个字符串参数且没有返回类型的代理。我们将对类作如下修改:
public class MyClass { public delegate void LogHandler(string message); public void Process(LogHandler logHandler) { if (logHandler != null) logHandler("Process() begin"); // 这里还有其他东西? if (logHandler != null) logHandler ("Process() end"); } }
虽然在调用函数前,我们需要检查代理是否为空(即不指向一个函数),但使用代理与直接调用函数相似。
要调用 Process() 函数,我们需要声明一个与代理相匹配的记录函数,然后创建代理的实例,以指向该函数。然后,将此代理传递给 Process() 函数。
class Test { static void Logger(string s) { Console.WriteLine(s); } public static void Main() { MyClass myClass = new MyClass(); MyClass.LogHandler lh = new MyClass.LogHandler(Logger); myClass.Process(lh); } }
Logger() 函数是一个我们要从 Process() 函数中调用的函数,我们对它进行了声明,使其与代理相匹配。在 Main() 中,我们创建代理的一个实例,然后将该函数传递给代理构造器,使其指向该函数。最后,我们将代理传递给 Process() 函数,该函数接着调用 Logger() 函数。
如果您习惯于使用 C++ 语言,您可能会认为代理很像函数指针,这种想法非常接近于事实。但是,代理并不“仅仅”是函数指针,它还提供了其它多种功能。
传递状态
在上面的简单示例中,Logger() 函数仅仅将字符串写出。其它函数可能要将信息记录到文件中,但是要进行这种操作,该函数需要知道信息将写入的文件。
对于 Win32® 而言,当您传递函数指针时,可随之传递状态。但是对于 C#,这就没有必要了,因为代理既可指向静态函数,“也”可指向成员函数。以下是一个有关如何指向成员函数的示例:
class FileLogger { FileStream fileStream; StreamWriter streamWriter; public FileLogger(string filename) { fileStream = new FileStream(filename, FileMode.Create); streamWriter = new StreamWriter(fileStream); } public void Logger(string s) { streamWriter.WriteLine(s); } public void Close() { streamWriter.Close(); fileStream.Close(); } } class Test { public static void Main() { FileLogger fl = new FileLogger("process.log"); MyClass myClass = new MyClass(); MyClass.LogHandler lh = new MyClass.LogHandler(fl.Logger); myClass.Process(lh); fl.Close(); } }
FileLogger 类仅封装文件。Main() 会被修改,以使代理指向 FileLogger 的
fl
实例上的 Logger() 函数。当从 Process() 中调用此代理时,将会调用成员函数并把字符串记录到相应的文件中。
其优点在于,我们不必更改 Process() 函数 - 无论代理引用的是静态函数还是成员函数,它们的代码都是相同的。
多播
虽然指向成员函数的功能已让人感到满意,但利用代理,您还可以巧妙地完成其它一些任务。在 C# 中,代理是“多播”的,这表示它们可同时指向一个以上的函数(即基于 System.MulticastDelegate 类型)。多播代理将维护一个函数列表。当调用该代理时,将会调用列表中的所有函数。我们可以添加第一个示例中的记录函数,然后调用这两个代理。要将两个代理组合起来,可使用 Delegate.Combine() 函数。其代码如下:
MyClass.LogHandler lh = (MyClass.LogHandler) Delegate.Combine(new Delegate[] {new MyClass.LogHandler(Logger), new MyClass.LogHandler(fl.Logger)});
啊呀,真的是很难看!幸好 C# 提供了一种更好的语法,而不用将以上语法强加给用户。无需调用 Delegate.Combine(),仅使用
+=
即可组合这两个代理:
MyClass.LogHandler lh = null; lh += new MyClass.LogHandler(Logger); lh += new MyClass.LogHandler(fl.Logger);
这样就简洁多了。要从多播代理中删除一个代理,可调用 Delegate.Remove() 或使用
-=
运算符(我知道我想用哪一个)。
当您调用多播代理时,就会按出现顺序对调用列表中的代理进行并行调用。如果此过程中出现了错误,执行过程即被中断。
如果您想更严格地控制调用顺序(例如要进行万无一失的调用),则可以从代理中获取调用列表,然后自行调用这些函数。以下是一个示例:
foreach (LogHandler logHandler in lh.GetInvocationList()) { try { logHandler(message); } catch (Exception e) { // 在这里处理异常情况吗? } }
代码将每次调用包装在一个 try-catch 对中,这样在一个处理程序中引发的异常就不会妨碍对其它处理程序的调用。
事件
我们已经对代理进行了较长时间的讨论,现在该谈一谈事件了。有一个明显的问题:“既然我们已经有了代理,为什么还需要事件?”
要得到这个问题的最佳答案,只需考虑用户界面对象所发生的事件。例如,一个按钮可能有公共的“Click”代理。我们可将一个函数挂接到该代理上,这样当单击此按钮时,就可以调用该代理。例如:
Button.Click = new Button.ClickHandler(ClickFunction);
它表示当单击此按钮时,将调用 ClickFunction()。
小测验:上述代码是否存在问题?我们忘记了什么?
答案是,我们忘记使用 += 而直接分配了代理。这表示其它任何挂接到“Button.Click”的代理现在都将解除挂接。“Button.Click”应该是公共的,以便其它对象可以对其进行访问,因此上述情况将无法避免。同样,要删除代理,用户可能会编写以下代码:
Button.Click = null;
它将删除所有代理。
这种情形尤为糟糕,因为在大多数情况下,只有一个代理进行了挂接,而且问题通常不会明显地表现为错误。随后,当挂接了另一个代理时,情况就开始变坏。
事件在代理模型之上添加一个保护层。以下是支持事件的对象示例:
public class MyObject { public delegate void ClickHandler(object sender, EventArgs e); public event ClickHandler Click; protected void OnClick() { if (Click != null) Click(this, null); } }
ClickHandler 代理使用事件代理的标准模式来定义事件的签名。其名称的末尾是处理程序,它带有两个参数。第一个参数是发送对象的对象,第二个参数用于传递事件的伴随信息。这种情况下没有要传递的信息,因此直接使用 EventArgs,但是如果有数据要传递,则使用从 EventArgs 派生的类(例如 MouseEventArgs)。
“Click”事件的声明执行两项操作:首先,它声明一个名为“Click”的代理成员变量,该变量从类的内部进行使用。其次,它声明一个名为“Click”的事件,该事件可按照常规访问规则从类的外部进行使用(在此例中,事件为公共事件)。
通常,将包括 OnClick() 等函数,以便使类型或派生类型能够触发事件。由于“Click”是代理,您将会注意到,用来触发事件的代码与代理的代码相同。
与代理类似,我们使用 += 和 -= 来挂接到事件或解除事件挂接,但与代理不同的是,仅可对事件执行这些操作。这可确保不会发生先前所讨论的两种错误。
使用事件是一种直截了当的方法。
class Test { static void ClickFunction(object sender, EventArgs args) { // process the event here. } public static void Main() { MyObject myObject = new MyObject(); myObject.Click += new MyObject.ClickHandler(ClickFunction); } }
我们创建一个与代理签名相匹配的静态函数或成员函数,然后用 += 向事件中添加代理的一个新实例。
本月的讨论到此结束。下个月,我们将讨论事件的内部工作方式以及实现事件的一些高级方法(提示:事件有时与代理略有不同,事件往往具有额外的保护)。如果您迫不及待地要了解这些信息,可参看示例代码,因为下个月的代码也包含在其中。
网站收集袋
不幸的是,我的膝上型电脑在这个月出了点小麻烦,所有发给我的站点都丢了。如果您曾给我发送 URL,但还没有在此站点看到您的 URL,请将它再次发送给我:ericgu@microsoft.com。
Eric Gunnerson 是 C# 编译器小组的 QA 组长,也是 C# 设计小组的成员和 C# 程序员简介(英文)的作者。他很早就从事编程工作,甚至知道 8 英寸磁盘的模样,还能用一只手装磁带。