目录 译者的话 概述 委托(Delegates) 直接调用方法-不用委托 最基本的委托 调用静态方法 调用成员方法 多路广播 事件(Events) 惯例 一个简单事件的示例 第二个事件例子 结论
译者的话(By LuBen) 委托和事件对于初学者来说,总是难以理解。以前看到过关于委托方面的文章.NET Delegates: A C# Bedtime Story,写的非常好,网上也有其中文版本。但是对于初学者来说,下面这篇文章似乎更加通俗易懂,所以特别翻译一下,供初学者学习。 英文原文地址:Delegates and Events in C# / .NET 今天,各种各样事件驱动(event driven)的编程方式充斥着我们的视野。C# 通过支持事件与委托(events and delegates) 给事件驱动编程世界注入了新的活力。本文重点探讨当我们给普通UI控件添加一个事件处理程序(event handle)时到底发生了什么。 通过一个简单的模拟,给Button类添加AddOnClick或类似事件,我们将探寻发生在幕后的真实故事。这将帮助我们更好的理解使用了多路广播委托(multi cast delegates)的事件处理程序的本质:) C#中的委托类似于C或者C++中的函数指针。利用委托,程序员可以在委托对象中封装一个方法的引用。 然后委托对象将被传给调用了被引用方法的代码,而不需要知道在编译时刻具体是哪个方法被调用。(译者注:如果您不理解这段话,先看下去回过头再来理解) 在大多数情况下,当我们调用一个方法时,我们是直接指定调用的方法。 例如,如果类MyClass有个名为Process的方法,我们通常会这样调用它 (SimpleSample.cs): using System; namespace Akadia.NoDelegate { public class MyClass { public void Process() { Console.WriteLine("Process() begin"); Console.WriteLine("Process() end"); } } public class Test { static void Main(string[] args) { MyClass myClass = new MyClass(); myClass.Process(); } } } 在大多数情况下,这样子做就足够了。但是,有些时候,我们并不想去直接调用方法 - 我们想把它传给某个其他的对象,让其能够调用它。这个在事件驱动的系统中非常有用的, 象GUI(graphical user interface),当我们想在用户点击按钮时执行某段代码,或者是当我们想记录日志,但是又不知道该怎么去记录时。 委托一个很有趣又有用的特性是它并不知道或者说是关心它引用的方法的类别。 任何方法都可以,只要这个方法的参数类型和返回值类型能够匹配它。这个特性使得委托十分适合匿名调用("anonymous" invocation)。 单路广播委托型构(signature)如下: delegate result-type identifier ([parameters]); 这里:
- result-type: 返回值的类型,和方法的返回值类型一致
- identifier: 委托的名称
- parameters: 参数,要引用的方法带的参数
例:
public delegate void SimpleDelegate () 这个声明定义了一个名为SimpleDelegate的委托,它可以封装任何不带参数不返回值的方法。 |
public delegate int ButtonClickHandler (object obj1, object obj2) 这个声明定义了一个名为ButtonClickHandler的委托,它可以封装任何带2个Objec参数返回int类型值的方法。 |
委托允许我们只需指定调用的方法是什么样子,而不需要指定具体调用哪个方法。委托的声明看起来就像是方法的声明,除了一个情形,那就是在我们要声明的方法正好是这个委托能够引用的方法的时候。 定义和使用委托有三个步骤:
一个很基础的示例(SimpleDelegate1.cs): using System;
namespace Akadia.BasicDelegate { // 声明 public delegate void SimpleDelegate();
class TestDelegate { public static void MyFunc() { Console.WriteLine("I was called by delegate ..."); }
public static void Main() { // 实例化 SimpleDelegate simpleDelegate = new SimpleDelegate(MyFunc);
// 调用 simpleDelegate(); } } } 编译测试: # csc SimpleDelegate1.cs # SimpleDelegate1.exe I was called by delegate ... 下面是一个复杂点的例子(SimpleDelegate2.cs), 声明了一个带一个string类型参数不返回值的委托: using System;
namespace Akadia.SimpleDelegate { // 委托定义 public class MyClass { // 声明一个带string参数不返回值的委托 public delegate void LogHandler(string message);
// 使用委托就像使用方法一样。不过我们在调用前需要检查委托是否为空(委托没有引用任何方法) public void Process(LogHandler logHandler) { if (logHandler != null) { logHandler("Process() begin"); }
if (logHandler != null) { logHandler ("Process() end"); } } }
// 测试应用程序,使用上面定义的委托 public class TestApplication { // 静态方法:将被委托引用。为了调用Process()方法, // 我们定义一个记录日志的方法:和委托型构匹配的Logger() static void Logger(string s) { Console.WriteLine(s); }
static void Main(string[] args) { MyClass myClass = new MyClass();
// 创造委托实例,引用上面定义的日志方法。该委托将被传给Process()方法。 MyClass.LogHandler myLogger = new MyClass.LogHandler(Logger); myClass.Process(myLogger); } } } 编译测试: # csc SimpleDelegate2.cs # SimpleDelegate2.exe Process() begin Process() end 在上面简单的例子中,方法Logger( ) 仅仅实现写出字符串。我们可能需要一个不同的方法把把日志信息写入到文件中,如果要实现如此,则这个方法需要知道把日志信息写入到哪一个文件(SimpleDelegate3.cs): using System; using System.IO;
namespace Akadia.SimpleDelegate { // 委托的定义 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"); } } }
// 封装文件I/O操作的类 public 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(); } }
// 委托指向FileLogger类实例f1的Logger()方法。 // 当委托在Process()被调用时,成员方法Logger()也将被调用,日志被写入到指定的文件中。 public class TestApplication { static void Main(string[] args) { FileLogger fl = new FileLogger("process.log");
MyClass myClass = new MyClass();
// 创造委托实例,引用上面定义的日志方法。该委托将被传给Process()方法。 MyClass.LogHandler myLogger = new MyClass.LogHandler(fl.Logger); myClass.Process(myLogger); fl.Close(); } } } 这部分很酷的一点是我们并不需要改变Process()方法,不管委托引用的是static还是member方法,委托定义调用部分代码都一样。 测试编译: # csc SimpleDelegate3.cs # SimpleDelegate3.exe # cat process.log Process() begin Process() end 多路广播(Multicasting) 能够引用成员方法已经很不错了,但是运用委托我们还可以做到更多。 在C#中,委托是多路广播的(multicast),也就是说它们可以一次同时指向多个方法。多路广播委托维护着一个方法列表,这些方法在该委托被调用时都将被调用。 using System; using System.IO;
namespace Akadia.SimpleDelegate { // 委托的定义 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"); } } }
// 封装文件I/O操作的类 public class FileLogger { FileStream fileStream; StreamWriter streamWriter;
// Constructor 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(); } }
// 调用多个委托的测试应用程序 public class TestApplication { // 委托中将要使用的静态方法 static void Logger(string s) { Console.WriteLine(s); }
static void Main(string[] args) { FileLogger fl = new FileLogger("process.log");
MyClass myClass = new MyClass();
// 创造一个委托实例,引用TestApplication // 中定义的静态方法Logger()和FileLogger类的实例f1的成员方法。 MyClass.LogHandler myLogger = null; myLogger += new MyClass.LogHandler(Logger); myLogger += new MyClass.LogHandler(fl.Logger);
myClass.Process(myLogger); fl.Close(); } } }
测试编译: # csc SimpleDelegate4.cs # SimpleDelegate4.exe Process() begin Process() end # cat process.log Process() begin Process() end C#中的事件模型是以事件编程模型为基础的,事件编程模型在异步编程时非常普遍。这种编程模型源于 “出版者和订阅者(publisher and subscribers)”思想。在这个模型中,出版者(subscribers)处理一些逻辑发布一个“事件”,它们仅发布这些的事件给订阅了该事件的订阅者(publishers)。 在C#中,任何对象都能发布一组其他应用程序能够订阅的事件。 当这些发布类(发布这些事件的类)触发了该事件时,所有订阅了该事件的应用程序都将被通知到。下面这副图展现了这个机制:
下面是关于使用事件的一些重要惯例:
- .NET Framework中的Event Handlers不返回任何值,带有2个参数
- 第一个参数是事件的源,也就是发布该事件的对象(译者注:具体来讲,应该是发布事件的类的实例)
- 第二个参数是一个继承了EventArgs的对象
- 事件作为发布它的类的属性存在。
- 关键字event(事件)控制着事件订阅类如何访问该事件属性
译者注:EventHandler是.NET Framework自带的事件委托类型。如上所叙,它带有2个参数,如果发布事件的类没有数据需要传送给订阅类,那么使用系统自带的EventHandler委托就足够了。如果类似事件第二个例子一样,需要把Clock数据传给订阅类,则通常使用自定义的委托,包含一个继承EventArgs的参数类,让这个参数类封装要传送的数据(如示例2)。 下面我们不用委托,而是使用事件来修改实现上面的日志例子 using System; using System.IO;
namespace Akadia.SimpleEvent { /* ========= Publisher of the Event ============== */ public class MyClass { // 定义一个名为LogHandler的委托,它封装带有一个string参数不返回任何值的方法 public delegate void LogHandler(string message); // 定义基于上面定义的委托的事件 public event LogHandler Log; // 代替使用委托作为参数的Process()方法。 // 调用事件,使用OnXXXX方法,XXXX是事件的名称 public void Process() { OnLog("Process() begin"); OnLog("Process() end"); } // 防止事件为空,创建OnXXXX方法调用事件 protected void OnLog(string message) { if (Log != null) { Log(message); } } } // 封装文件I/O操作的类 public 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(); } } /* ========= Subscriber of the Event ============== */ // 现在添加委托实例给事件变得更加简单清晰 public class TestApplication { static void Logger(string s) { Console.WriteLine(s); } static void Main(string[] args) { FileLogger fl = new FileLogger("process.log"); MyClass myClass = new MyClass(); // 订阅Logger方法和f1.Logger myClass.Log += new MyClass.LogHandler(Logger); myClass.Log += new MyClass.LogHandler(fl.Logger);
// 触发Process()方法 myClass.Process(); fl.Close(); } } } 编译测试: # csc SimpleEvent.cs # SimpleEvent.exe Process() begin Process() end # cat process.log Process() begin Process() end 假设我们要创建一个Clock类,当本地时间每变化一秒钟,该类就使用事件来通知潜在的订阅者。 请看示例: using System; using System.Threading;
namespace SecondChangeEvent { /* ======================= Event Publisher =============================== */
// 被其他类观察的钟(Clock)类,改类发布一个事件:SecondChange。观察该类的类订阅了该事件。 public class Clock { // 代表小时,分钟,秒的私有变量 private int _hour; private int _minute; private int _second;
// 定义名为SecondChangeHandler的委托,封装不返回值的方法, // 该方法带参数,一个clock类型对象参数,一个TimeInfoEventArgs类型对象 public delegate void SecondChangeHandler ( object clock, TimeInfoEventArgs timeInformation );
// 要发布的事件 public event SecondChangeHandler SecondChange;
// 触发事件的方法 protected void OnSecondChange( object clock, TimeInfoEventArgs timeInformation ) { // Check if there are any Subscribers if (SecondChange != null) { // Call the Event SecondChange(clock,timeInformation); } }
// 让钟(Clock)跑起来,每隔一秒钟触发一次事件 public void Run() { for(;;) { // 让线程Sleep一秒钟 Thread.Sleep(1000);
// 获取当前时间 System.DateTime dt = System.DateTime.Now;
// 如果秒钟变化了通知订阅者 if (dt.Second != _second) { // 创造TimeInfoEventArgs类型对象,传给订阅者 TimeInfoEventArgs timeInformation = new TimeInfoEventArgs( dt.Hour,dt.Minute,dt.Second);
// 通知订阅者 OnSecondChange (this,timeInformation); }
// 更新状态信息 _second = dt.Second; _minute = dt.Minute; _hour = dt.Hour;
} } }
// 该类用来存储关于事件的有效信息外, // 还用来存储额外的需要传给订阅者的Clock状态信息 public class TimeInfoEventArgs : EventArgs { public TimeInfoEventArgs(int hour, int minute, int second) { this.hour = hour; this.minute = minute; this.second = second; } public readonly int hour; public readonly int minute; public readonly int second; }
/* ======================= Event Subscribers =============================== */
// 一个订阅者。DisplayClock订阅了clock类的事件。它的工作是显示当前事件。 public class DisplayClock { // 传入一个clock对象,订阅其SecondChangeHandler事件 public void Subscribe(Clock theClock) { theClock.SecondChange += new Clock.SecondChangeHandler(TimeHasChanged); }
// 实现了委托匹配类型的方法 public void TimeHasChanged( object theClock, TimeInfoEventArgs ti) { Console.WriteLine("Current Time: {0}:{1}:{2}", ti.hour.ToString(), ti.minute.ToString(), ti.second.ToString()); } }
// 第二个订阅者,他的工作是把当前时间写入一个文件 public class LogClock { public void Subscribe(Clock theClock) { theClock.SecondChange += new Clock.SecondChangeHandler(WriteLogEntry); }
// 这个方法本来应该是把信息写入一个文件中 // 这里我们用把信息输出控制台代替 public void WriteLogEntry( object theClock, TimeInfoEventArgs ti) { Console.WriteLine("Logging to file: {0}:{1}:{2}", ti.hour.ToString(), ti.minute.ToString(), ti.second.ToString()); } }
/* ======================= Test Application =============================== */
// 测试拥有程序 public class Test { public static void Main() { // 创建clock实例 Clock theClock = new Clock();
// 创建一个DisplayClock实例,让其订阅上面创建的clock的事件 DisplayClock dc = new DisplayClock(); dc.Subscribe(theClock);
// 创建一个LogClock实例,让其订阅上面创建的clock的事件 LogClock lc = new LogClock(); lc.Subscribe(theClock);
// 让钟跑起来 theClock.Run(); } } } 最后一个例子中的Clock类能够简单的实现打印时间而不是触发事件,所以为什么对关于使用委托的介绍感到烦躁呢?使用发布/订阅模式的最大好处就是当一个事件触发时能够通知任意数目的订阅类。这些订阅类不需要知道Clock类如何工作,而这个Clock类也不需要知道订阅者们如何来响应这个事件。类似的,按钮能够发布一个OnClick事件,任意数目不想关的对象能够订阅这个事件,并且在这个按钮被点击时被通知到。 发布者和订阅者通过委托很好的实现了解耦。这样大大增强了可扩展性和健壮性。 Clock类能够改变其洞察时间的方式而不会干扰到那些订阅类,订阅类也能改变其对时间改变事件作出的反应而不用打扰Clock类。两个类相互独立,互不干扰,使得维护代码变得更加容易。 |