事件与代理(下)
与此关联的上篇文章可以在如下网址找到
http://mqingqing123.cnblogs.com/archive/2005/08/10/211298.html
在上篇中可以看到,事件就像只能应用 +=
和 -=
运算符的委托。我们了解到当您这样使用委托时:
MyClass.LogHandler lh = null;
lh += new MyClass.LogHandler(Logger);
lh += new MyClass.LogHandler(fl.Logger);
编译器将把+=
的使用转换为对 Delegate.Combine() 的调用。因此我们可能期望对事件会发生相同的事情;
那么就让我们来看看编译器做了些什么。使用我们上篇的代码:
public static void Main()
{
MyObject myObject = new MyObject();
myObject.Click += new MyObject.ClickHandler(ClickFunction);
}
我们生成代码,然后通过 Microsoft 中间语言反汇编程序 (ILDASM)来运行它,看看会出现什么。让我们大吃一惊的是,我们找到的不是对 Delegate.Combine() 的调用,而是对 MyObject 类中名为 add_Click() 的函数的调用。哦,我不知道那个函数会做什么。
如果我们看看 MyObject 的信息,便会注意到它具有名为 add_Click() 和 remove_Click() 的函数。当我们查看 add_Click() 中的 IL 时,我们注意到该函数中具有对 Delegate.Combine() 的调用。
这里所发生的情况与使用属性时很像;我们不是直接访问字段,而是用 accessor 函数来获取或设置值。在这种情况下,我们调用 add 函数以挂钩到事件,调用 remove 函数以从事件解除挂钩,这就是为什么除了 +=
或者 -=
不能对事件做任何其他事情的原因。我们所认为的事件,是作为 add 函数和 remove 函数实现的。
在 C# 中有两种实现事件的方式,这两种方法之间在细节之处稍有区别。首先,是简单情况。
简单事件
在简单事件的情况中,编译器为我们完成所有的工作。当我们编写类似以下的代码时:
编译器将声明名为 Log 的 private 委托字段,但是它也将声明名为 add_Log() 和 remove_Log() 的函数,这些函数调用适当的 Delegate 函数添加或移除私有字段中的委托。最后,它将在元数据中声明名为 Log 的事件,该元数据具有对 add 和 remove 函数到引用,因此将有在对象浏览器中显示的事件,编译器可以找到与事件相关联的 add 函数和 remove 函数(这也是属性的运行方式)。
让我们将上篇的记录示例修改为使用事件而不是委托:
using System;
using System.IO;
public class MyClass
{
public delegate void LogHandler(string message);
public event LogHandler Log;
public void Process()
{
OnLog("Process() begin");
// other stuff here
OnLog("Process() end");
}
protected void OnLog(string message)
{
if (Log != null)
Log(message);
}
}
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
{
static void Logger(string s)
{
Console.WriteLine(s);
}
public static void Main()
{
FileLogger fl = new FileLogger("process.log");
MyClass myClass = new MyClass();
myClass.Log += new MyClass.LogHandler(Logger);
myClass.Log += new MyClass.LogHandler(fl.Logger);
myClass.Process();
fl.Close();
}
}
我们没有让 Process() 函数采用委托作为参数,而是声明了 Log 事件。在 Main() 中,光把委托的实例添加到事件,而不亲自管理,这样更简单,也更清楚(后者可能会把它们搞错)。
让编译器完成所有您通常想做的工作,但是有时您需要亲自去做。
高级事件
编译器处理简单事件的方式所带来的问题之一是它声明事件的委托字段,这将占用对象中的空间。如果对象仅支持一个或两个事件,不成问题,但是许多用户界面对象可能支持几十甚至几百个不同的事件。对于给定对象一次仅挂钩几个事件,因此那里有许多空间被浪费掉了。
高级语法让程序员接管整个的过程,包括委托的存储。现在我们开始动手,下面是与编译器功能完全一样的高级语法:
using System;
using System.Runtime.CompilerServices;
public class MyClass
{
public delegate void LogHandler(string message);
private LogHandler log;
public event LogHandler Log
{
[MethodImpl(MethodImplOptions.Synchronized)]
add
{
log = (LogHandler) Delegate.Combine(log, value);
}
[MethodImpl(MethodImplOptions.Synchronized)]
remove
{
log = (LogHandler) Delegate.Remove(log, value);
}
}
protected void OnLog(string message)
{
if (log != null)
log(message);
}
}
在该示例中,我们为事件编写了我们自己的 add() 和 remove() 访问器。Delegate.Combine() 和 Delegate.Remove() 调用在很大程度上是自我解释的。需要 MethodImpl 属性以便使 add 和 remove 函数线程安全。如果没有此属性,两个线程将可以同时调用 add,但是如果执行时间刚好(或者刚好是错的,这取决于您看待它的方式),则结果可能不正确。
该示例向我们显示了如何编写更多的代码却不带来任何好处。为了得到我们想要的好处,即使用更少的存储,我们将需要以其他方式存储委托。
假设我们的对象可以有许多和它相关联的事件,但其中很少是活动的。因此我们将需要一种方式来存储与事件关联的委托,而不用对每个委托使用单独的字段。有个简单的方法可以做到这一点,那就是将它们存储在哈希表中,但是编写封装哈希表的类更清楚,并且可以提供更简单的接口。这是最终结果:
using System; using System.Runtime.CompilerServices; using System.Collections; class DelegateCache { private Hashtable delegateStorage = new Hashtable(); public Delegate Find(object key) { return((Delegate) delegateStorage[key]); } public void Add(object key, Delegate myDelegate) { delegateStorage[key] = Delegate.Combine((Delegate) delegateStorage[key], myDelegate); } public void Remove(object key, Delegate myDelegate) { delegateStorage[key] = Delegate.Remove((Delegate) delegateStorage[key], myDelegate); } } public class MyClass { public delegate void LogHandler(string message); private DelegateCache delegateCache = new DelegateCache(); private static object logEventKey = new object(); // unique key public event LogHandler Log { [MethodImpl(MethodImplOptions.Synchronized)] add { delegateCache.Add(logEventKey, value); } [MethodImpl(MethodImplOptions.Synchronized)] remove { delegateCache.Remove(logEventKey, value); } } protected void OnLog(string message) { LogHandler lh = (LogHandler) delegateCache.Find(logEventKey); if (lh != null) lh(message); } public void Process() { OnLog("Process() begin"); // other stuff here... OnLog("Process() end"); } } class Test { static void Logger(string s) { Console.WriteLine(s); } public static void Main() { MyClass myClass = new MyClass(); myClass.Log += new MyClass.LogHandler(Logger); myClass.Log += new MyClass.LogHandler(Logger); myClass.Process(); myClass.Log -= new MyClass.LogHandler(Logger); myClass.Process(); } }
DelegateCache 类为我们完成了更新的工作,并可以被其他的类用于存储它们的委托。Add 和 Remove 访问器使用 helper 类存储委托。logEventKey 变量用于生成对该事件唯一的键,但它不占用对象中的空间,原因是它是静态变量。然后此键被用于确保在调用 DelegateCache 实例中的函数时获取正确的委托。该示例的输出为下列代码:
Process() begin Process() begin Process() end Process() end Process() begin Process() end
也可以实现应用程序范围而非特定于单个实例的缓存。为了做到这一点,将不得不根据实例和键两者来存储委托。