C# 事件 vs 委託

iDotNetSpace發表於2009-12-30

我們在以前的文章中看到了委託以及它們的實現。但是如果你在網頁上搜尋有關委託的資訊,你肯定會注意到它們總是與“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框架中使用時)。

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-623971/,如需轉載,請註明出處,否則將追究法律責任。

相關文章