c# 事件和委托的区别,使用事件的好处

网上一些介绍事件 (event) 的文章让人看来觉得事件和委托好像有很大的不同。通常,事件被作为一种特殊的类型或结构来看待。但事实上,事件只是委托的一个修饰符 (modifier),此外编译器增加了一些限制,还增加了 add 和 remove 两个访问器(类似属性的 get 和 set)。


事件和委托探索

从语法上看,事件的用法和多播委托差不多。此外,它们还支持相同的运算符 (+ 和 -)。
在下面的代码中,我们可以看到 msgNotifier (事件) 和 msgNotifier2 (委托) 的用法。

View Code
 1 using System;
 2 
 3 namespace EventAndDelegate
 4 {
 5   delegate void MsgHandler(string s);
 6 
 7   class Class1
 8   {
 9    public static event MsgHandler msgNotifier;
10    public static MsgHandler msgNotifier2;
11    [STAThread]
12    static void Main(string[] args)
13    {
14     Class1.msgNotifier += new MsgHandler(PipeNull);
15     Class1.msgNotifier2 += new MsgHandler(PipeNull);
16     Class1.msgNotifier("test");
17     Class1.msgNotifier2("test2");
18    }
19  
20    static void PipeNull(string s)
21    {
22     return;
23    }
24   }
25 }

我们来看一下这段代码的 Main 方法的 IL 代码,你会发现委托 msgNotifiermsgNotifier2 的用法几乎差不多。

View Code
.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 修饰符的好处

首先,事件可以包含在接口声明中,而字段不能。这是 event 修饰符带来的最重要的变化之一。例如:

View Code
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) {}
}

事件的调用

此外,事件只能在其声明类中调用;而如果是委托,则任何有权访问委托的对象都可以调用委托。例如:

View Code
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
   }
  }
}

This restriction on invocations is quite strong. Even derived classes from the class declaring the event aren't allowed to fire the event. A way to deal with this is to have a protected virtual method to trigger the event.

这种调用限制是相当严格的。即使在基类中声明的事件,在其派生类中也不能触发。要想绕过这个限制,一种解决方法是使用一个受保护的虚方法来触发事件。

另外,类似于属性的 get 和 set 方法,事件提供了 addremove 方法。

You are allowed to override these accessors, as shown in examples 2 and 3 on this C# event modifier reference on MSDN. Although I don't see how example 2 is useful, you could imagine that you could have a custom add to send some notification or write a log entry, for example, when a listener is added to your event.

你可以覆盖这些方法,像 MSDN 上的 C# 事件修饰符参考这篇文章中给出的示例 2 和示例 3 一样。我看不出示例 2 有什么用,但你可以想象一下,你可以在一些情况下使用自定义的 add 方法来发送某些通知或写入一个日志项,比如说当某个对象加入到事件中的时候。


The add and remove accessors need to be customized together, otherwise you get error CS0065 ('Event.TestClass.msgNotifier' : event property must have both add and remove accessors).
Looking at the IL for a previous example, where the event accessors weren't customized, I noticed compiler generated methods (add_msgNotifier and remove_msgNotifier) for the msgNotifier event. But they weren't used, and whenever the event was accessed the same IL code was duplicated (inlined).
But when you customize these accessors and look at the IL again, you'll notice that the generated accessors are now used when you access the event. For example, this code :

View Code
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)
   {
   }
  }
}

brings the following IL for the Main method:

View Code
{
  .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

Event signature
Finally, even though C# allows it, the .NET framework adds a restriction on the signature of delegates that can be used as events. The signature should be foo(object source, EventArgs e), where source is the object that fired the event and econtains any additional information about the event.


总结
我们已经看到了,event 关键字是委托类型的一个修饰符,加上这个修饰符就可以在接口中声明委托;event 关键字限制了你只能在事件的声明类中调用事件;此外它提供了一组访问器 (addremove),并且限制了委托签名。

 

Links
Events Tutorialon MSDN.

Event keyword reference on MSDN.

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.

 

 

转载于:https://www.cnblogs.com/moyos/archive/2012/10/28/2743919.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值