[译]C# 事件 vs 委托

原文地址:http://blog.monstuff.com/archives/000040.html

我们在以前的文章中看到了委托以及它们的实现。但是如果你在网页上搜索有关委托的信息,你肯定会注意到它们总是与“event”结构相关联。
联机事件教程使得事件尽管与常规的委托实例有关系,但是还是有许多区别。事件经常被解释得就好像它们是一种特殊的类型或结构。但是我们将看到它们只是委托类型的一种修饰器,它们仅仅是添加了一些编译器强制执行的限制和两个存取器(与属性的get和set相似)。

首先看看事件 vs 常规委托
当我完成了前一篇关于委托的文章时,另一个C#构造也进入了我的计划:事件。事件看起来确实与委托有关,我没能找出它们之间的不同。

从它们的语法看来,事件就好像是一个留有代表多播委托的委托组合字段。它们同样支持委托的(+和-)组合操作。
在接下来的例子程序(没有任何有用的功能)中,我们将看见msgNotifier(使用event结构)和msgNotifier2(普通委托)看起来有个一致的意图和目的。

代码
namespace EventAndDelegate
{
  
delegate void MsgHandler(string s);

  
class Class1
  {
   
public static event MsgHandler msgNotifier;
   
public static MsgHandler msgNotifier2;
   [STAThread]
   
static void Main(string[] args)
   {
    Class1.msgNotifier 
+= new MsgHandler(PipeNull);
    Class1.msgNotifier2 
+= new MsgHandler(PipeNull);
    Class1.msgNotifier(
"test");
    Class1.msgNotifier2(
"test2");
   }
 
   
static void PipeNull(string s)
   {
    
return;
   }
  }
}

查看在上述代码中Main方法的IL代码,你会注意到msgNotifier和msgNotifier2都是委托,msgNotifier2使用的是同样的方式。

代码
.method private hidebysig static void Main(string[] args) cil managed
{
  .entrypoint
  .custom instance 
void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
  
// Code size 95 (0x5f)
  .maxstack 4
  IL_0000: ldsfld 
class EventAndDelegate.MsgHandler  EventAndDelegate.Class1::msgNotifier
  IL_0005: ldnull
  IL_0006: ldftn 
void EventAndDelegate.Class1::PipeNull(string)
  IL_000c: newobj instance 
void EventAndDelegate.MsgHandler::.ctor(object,
     native 
int)
  IL_0011: call 
class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
     
class [mscorlib]System.Delegate)
  IL_0016: castclass EventAndDelegate.MsgHandler
  IL_001b: stsfld 
class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier
  IL_0020: ldsfld 
class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier2
  IL_0025: ldnull
  IL_0026: ldftn 
void EventAndDelegate.Class1::PipeNull(string)
  IL_002c: newobj instance 
void EventAndDelegate.MsgHandler::.ctor(object,
     native 
int)
  IL_0031: call 
class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
     
class [mscorlib]System.Delegate)
  IL_0036: castclass EventAndDelegate.MsgHandler
  IL_003b: stsfld 
class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier2
  IL_0040: ldsfld 
class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier
  IL_0045: ldstr 
"test"
  IL_004a: callvirt instance 
void EventAndDelegate.MsgHandler::Invoke(string)
  IL_004f: ldsfld 
class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier2
  IL_0054: ldstr 
"test2"
  IL_0059: callvirt instance 
void EventAndDelegate.MsgHandler::Invoke(string)
  IL_005e: ret
// end of method Class1::Main

查看一下在MSDN上的C#关键字,它证明了event仅仅是一个修饰符。问题是这样使用后会带来什么方面的不同呢?

 

事件增加的值
事件与接口
首先,一个事件可以包含在接口声明中,而一个字段(译注:意指普通委托)不能。这是引入event修饰符后最重要的行为改变。例如:

代码
interface ITest
{
  
event MsgHandler msgNotifier; // compiles
  MsgHandler msgNotifier2; // error CS0525: Interfaces cannot contain fields
}
 
class TestClass : ITest
{
  
public event MsgHandler msgNotifier; // When you implement the interface, you need to implement the event too
  static void Main(string[] args) {}
}


事件引用
更多的是,一个事件仅能被包含其声明的类调用,然而委托字段可以任何有权限访问它的人调用。例如:

代码
using System;

namespace EventAndDelegate
{
  
delegate void MsgHandler(string s);

  
class Class1
  {
   
public static event MsgHandler msgNotifier;
   
public static MsgHandler msgNotifier2;

   
static void Main(string[] args)
   {
    
new Class2().test();
   }
  }
 
  
class Class2
  {
   
public void test()
   {
    Class1.msgNotifier(
"test"); // error CS0070: The event 'EventAndDelegate.Class1.msgNotifier' can only appear on the left hand side of += or -= (except when used from within the type 'EventAndDelegate.Class1')
    Class1.msgNotifier2("test2"); // compiles fine
   }
  }
}

在引用上这个限制是非常强的。甚至从声明事件的父类继承的派生类也不被允许触发事件。处理这种事情的一个方法是定义一个protected virtual方法来触发事件。

 

事件存取器
同时,事件将伴随着一对存取方法,它们有一个add和remove方法。这与属性非常相似,属性也提供了一对get和set方法。

你被允许重载引用自MSDN的关于C#事件修饰符的例2和例3中显示的那些存取器,尽管我没有看到例2有什么用处,但是你可以假设你能够针对某些通知编写自定义的添加方法或写入日志,例如,当一个监听者加入到你的事件中。

add和remove存取器需要同时自定义,否则将产生CS0065错误('Event.TestClass.msgNotifier' : 事件属性必须同时包含add和remove存取器)。
查看前一个例子的IL代码,里面的事件存取器并没有自定义,我注意到编译器自动生成的针对msgNotifier事件的方法(add_msgNotifier和remove_msgNotifier)。但是它们并没有使用,无论何时事件被访问,相同的IL代码将会被复制(内联方式)。
但是当你自定义这些存取器后再查看IL代码时,你将注意到自动生成的存取器当你访问事件的时候使用了。例如,代码如下:

代码
using System;

namespace Event
{
  
public delegate void MsgHandler(string msg);

  
interface ITest
  {
   
event MsgHandler msgNotifier; // compiles
   MsgHandler msgNotifier2; // error CS0525: Interfaces cannot contain fields
  }
 
  
class TestClass : ITest
  {
   
public event MsgHandler msgNotifier
   {
    add
    {
     Console.WriteLine(
"hello");
     msgNotifier 
+= value;
    }

   }
 
   
static void Main(string[] args)
   {
    
new TestClass().msgNotifier += new MsgHandler(TestDel);
   }
   
static void TestDel(string x)
   {
   }
  }
}

下面的是针对Main方法的IL代码:

代码
{
  .entrypoint
  
// Code size 23 (0x17)
  .maxstack 4
  IL_0000: newobj instance 
void Event.TestClass::.ctor()
  IL_0005: ldnull
  IL_0006: ldftn 
void Event.TestClass::TestDel(string)
  IL_000c: newobj instance 
void Event.MsgHandler::.ctor(object,
     native 
int)
  IL_0011: call instance 
void Event.TestClass::add_msgNotifier(class Event.MsgHandler)
  IL_0016: ret
// end of method TestClass::Main


事件签名
最后,尽管C#允许,.net框架增加一个被用于事件的关于委托签名的限制。这个签名应为foo(object source, EventArgs e),这里的source表示触发事件的对象,e包含一些关于事件的附加信息。

 

结论
我们已经看到event关键字是一个针对委托声明的修饰符,它允许它被包含在一个接口,限制它从声明它的类中引用,提供一对可定义的存取器(add和remove)并且强制委托签名(当在.net框架中使用时)。

 

链接
MSDN上的事件教程
MSDN上的event关键字引用 

 

Update:
One question that was left open and that was brought up by some readers was the rationale behind the restriction on event invocation: "Invoking an event can only be done from within the class that declared the event". I am still trying to get a definitive answer via some internal discussion lists, but here is the best idea that I got so far.
I think it is because of a syntaxic problem. When you put an access specifier ("private", "public", ...) on an event it controls who can register or listen to that event.
The question is how would you specify the access control for the invocation of that event. You can't use the same specifiers because it would be confusing.
The solution is to have the event invocation be completely restricted and allow the coder to write a custom invocation method on which he can easily control the access, which is the way it is now.

An alternate solution might have been to use some kind of attribute on the event [EventAccess(PublicInvocation)] or [EventAccess(ProtectedInvocation)]. But that seems uglier because it requires reflection to control the access at runtime.


Update:
Race condition in common event firing pattern:
As any other object, an event object needs to be treated with care in multi-threaded scenarios.

JayBaz and EricGu point out a frequent race condition mistake with event firing:
if (Click != null)
    Click(arg1, arg2);

Note that all the MSDN samples I have seen use the dangerous pattern. Posted by Julien on April 29, 2003. Permalink

posted @ 2009-12-29 16:40  Donot Forget  阅读(565)  评论(0编辑  收藏  举报