程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET相關問題: 事件存取器

.NET相關問題: 事件存取器

編輯:關於.NET

問:C# 使得在類上創建事件變得更為簡單,只需將關鍵字“event”添加到委托成員變量 聲明中即可。但是,它也允許使用類似屬性的語法,可以顯式地實現事件的 add 存取器和 remove 存取 器。 這樣做的原因是什麼?我只是重新創建 C# 編譯器為我生成的同一代碼,始終這樣做不可以嗎?

問:C# 使得在類上創建事件變得更為簡單,只需將關鍵字“event”添加到委托成員 變量聲明中即可。但是,它也允許使用類似屬性的語法,可以顯式地實現事件的 add 存取器和 remove 存取器。 這樣做的原因是什麼?我只是重新創建 C# 編譯器為我生成的同一代碼,始終這樣做不可以嗎 ?

答:在 C# 中,有幾個原因會使您希望或需要為事件實現您自己的 add 存取器和 remove 存取 器。我將列舉其中的幾個(這並不是一個詳細列表),以此說明自定義存取器如何實現新功能,以至提高 性能。

答:在 C# 中,有幾個原因會使您希望或需要為事件實現您自己的 add 存取器和 remove 存取器。我將列舉其中的幾個(這並不是一個詳細列表),以此說明自定義存取器如何實現新功能,以至 提高性能。

首先,考慮一個具有典型實例事件 MyEvent 的簡單的類 MyClass:

class 

MyClass
{
  public event EventHandler MyEvent;
  ...
}

當 C# 編譯器為 MyClass 類生成代碼時,在方式上,Microsoft® 中間語言 (MSIL) 的輸 出與使用類似圖 1 中的代碼所產生的內容是一致的。

Figure 1 Expanded Event Implementation

class MyClass
{
  private EventHandler _myEvent;
  public event EventHandler MyEvent
  {
    [MethodImpl(MethodImplOptions.Synchronized)]
    add
    {
      _myEvent = (EventHandler)Delegate.Combine(_myEvent, value);
    }
    [MethodImpl(MethodImplOptions.Synchronized)]
    remove
    {
      _myEvent = (EventHandler)Delegate.Remove(_myEvent, value);
    }
  }
  ...
}

對於這個簡單的事件語法,當您為調用 MyEvent 而在代碼中引用 MyEvent 時,C# 編譯器會 將其轉換成對基礎委托的調用,這樣,以下形式的調用

MyEvent(this, 

EventArgs.Empty);

實際上是向下編譯為類似如下形式的代碼:

_myEvent(this, 

EventArgs.Empty);

使用顯式實現時,您編寫自己的自定義 add 存取器和 remove 存取器,編 譯器不會知道事件的基礎數據存儲,因此,您不能再使用事件自身的名稱來調用事件,而必須直接引用委 托。

這間接說明了編寫自己的 add 存取器和 remove 存取器的一個主要原因:提供您自己的基礎 數據存儲。這樣做的一個原因是可能會存在以下這種情況:您的類中有許多公開的事件,而在任何時候通 常只有少量事件會在實例中同時使用。 在這種方案中,為維護每個事件的委托字段而帶來的內存開銷可 能會相當的大。下面以 Windows® Forms 的 Control 類為例進行說明。

在 Microsoft .NET Framework 2.0 中,Control 類公開了 69 個公共事件。如果這些事件中的每一個都有一個基礎委托字段 ,在 32 位系統上,這些事件會導致每個實例增加 276 字節的開銷。如果改為使 Control(或更准確地 說是其基類 System.ComponentModel.Component)維護一個鍵/值對列表,其中的“值”代表 委托。然後,Control 中的每一個事件有自定義的 add 存取器和 remove 存取器,存取器將所有事件的 已注冊的委托存儲到此列表(一個 System.ComponentModel.EventHandlersList 類實例)中。假定只對 特定 Control 中的少數事件進行處理,則這種通過 EventHandlersList 搜索特定委托的必要操作對性能 的影響會非常小,內存開銷僅限於用在 EventHandlersList 實現上的那些必要消耗。您可以在自己的類 中實現與此相同的模式,如圖 2 所示。

Figure 2 Custom Store for Event Delegates

class MyClass
{
  private static readonly _myEvent = new object();
  private EventHandlerList _handlers = new EventHandlerList();
  public event EventHandler MyEvent
  {
    add { _handlers.AddHandler(_myEvent, value); }
    remove { _handlers.RemoveHandler(_myEvent, value); }
  }
  private void OnMyEvent()
  {
    EventHandler myEvent = _handlers[_myEvent] as EventHandler;
    if (myEvent != null) myEvent(this, EventArgs.Empty);
  }
  ...
}

我已經遵從了 Windows Forms 中使用的約定,使每個事件都有一個靜態對象,在每個 EventHandlerList 中作為鍵來使用。由於這是靜態字段,因此,整個 AppDomain 才只有一個,而不是每 個實例都有一個。您也可以選擇對鍵使用字符串,但這樣的話,如果存在鍵入錯誤,則只有在運行過程中 相關查找失敗時才會發現問題;而使用靜態對象方法會在編譯時暴露大部分這類問題。

即使您希望使用一個委托作為事件的後備存儲,但對於顯式事件實現而言,某種相關的使用也會通過 屬性增加委托的大小。序列化是說明其重要性的最常見的例子。默認情況下,.NET 序列化機制對可序列 化的類中的所有字段進行序列化,而不考慮其可訪問性(這與 XML 序列化形成對比,在默認情況下,XML 序列化中只對公共字段和公共屬性進行序列化)。可序列化類中的所有字段必須或者是自身可序列化,或 者是可使用 NonSerializable 屬性免除序列化過程(另外,該類也可以實現 ISerializable 接口,以完 全控制哪些數據進行序列化,以及如何序列化),因為它們會由一個節省對象圖表的格式化程序來遍歷。

進行 .NET 序列化時,對於事件所遇到的麻煩是,事件是由私有委托字段支持的,而委托是可序 列化的。當為一個實例方法創建一個委托時,該委托將保持一個對在其上調用該方法的實例的引用,因此 ,當您對一個包含事件的對象進行序列化時,格式化程序在遍歷對象圖表的同時將繼續遍歷委托,並嘗試 對注冊該委托的任何實例進行序列化。如果不了解這一點,且沒有相應的計劃,則可能會出現一些負面結 果。如果您不需要這種方式,一個解決辦法就是用 NonSerializableAttribute 標記該事件的後備存儲, 如圖 3 所示。這將使格式化程序在對對象圖表進行序列化時跳過該字段。

Figure 3 Expanded Event Implementation

[Serializable]
class MyClass
{
  [NonSerializable]
  private EventHandler _myEvent;
  public event EventHandler MyEvent
  {
    [MethodImpl(MethodImplOptions.Synchronized)]
    add
    {
      _myEvent = (EventHandler)Delegate.Combine(_myEvent, value);
    }
    [MethodImpl(MethodImplOptions.Synchronized)]
    remove
    {
      _myEvent = (EventHandler)Delegate.Remove(_myEvent, value);
    }
  }
  ...
}

顯式事件實現的另一個應用是提供自定義的同步機制(或刪除一個這類機制)。在圖 1 中您 將注意到,add 存取器和 remove 存取器都標有 MethodImplAttribute,以指定這些存取器應進行同步。 對於實例事件,此屬性等同於對鎖定了當前實例的每個存取器的內容進行換行:

add { lock

(this) _myEvent += value; }
remove { lock(this) _myEvent -= value; }

如果您要編寫的類不會用於多線程,則采用這種 鎖不需要任何開銷,因此,您可以提供一個自定義的缺少 MethodImplAttribute 的實現。但是,在典型 的事件使用方案中,由於采用這種鎖的開銷是微不足道的,因此,極少將開銷作為自定義實現方式的有力 佐證。 然而,更重要的是,這種使用 lock(this) 的形式並不好,因為從效果上說,它會向您的類的所 有使用者公開(鎖定)的實現細節(他們也可以鎖定您的實例)。 如果必須進行鎖定操作,則對私有對 象進行鎖定可認為是一種更好的做法。而且,如果您沒有根據“this”引用進行同步,則默認 情況下,根據該引用進行同步的事件實現很可能是不正確的,因為,它不會按照與類中其余部分相同的方 式進行同步。

另一種情況是您可能需要顯式實現一個事件。考慮具有相同名稱事件的兩個接口:

interface I1
{
  event EventHandler MyEvent;
}
interface I2
{
  event EventHandler MyEvent;
}

如果希望用一個類來同時實現這兩個接口,可按如下方式進行:

class MyClass : I1, 

I2
{
  public event EventHandler MyEvent;
}

此處,MyClass.MyEvent 實現 I1.MyEvent 和 I2.MyEvent。但是,如果我希望 MyClass 分別 實現 I1.MyEvent 和 I2.MyEvent,該怎樣進行?實現這一目標的唯一方法是至少對其中一個接口使用顯 式接口實現(雖然對兩個接口都可以這樣做):

class MyClass : I1, I2
{
  public event EventHandler MyEvent;
  private EventHandler _i2MyEvent;
  event EventHandler I2.MyEvent
  {
    add { _i2MyEvent += value; }
    remove { _i2MyEvent -= value; }
  }
}

您可能需要實現自定義 add 存取器或 remove 存取器的第四個原因是:希望在對事件注冊委 托或從事件注銷委托的任何時候都執行自定義的邏輯處理。那麼這在哪些方案中會用到呢?有幾個方案屬 於這種情況。

考慮一下 Microsoft.Win32.SystemEvents 類。此類公開了十幾個公共靜態事件,在響應系統中的各 種情況時會引發這些事件:DisplaySettingsChanged、InstalledFontsChanged、 UserPreferenceChanging 等等。這些系統通知中有大部分是通過 Windows 消息通知給應用程序的。 對 於為這些通知引發 .NET 事件的 SystemEvents,它需要一個窗口來偵聽通知,並需要一個運行消息循環 的線程來響應通知。這類功能需要開銷,因此,SystemEvents 只有在對其中的一個事件注冊委托時才需 要初始化(創建廣播窗口、運行消息循環,等等)。而且,委托的 add 存取器是啟動用於確保類已初始 化的邏輯的最佳位置,如圖 4 所示。

Figure 4 Initialization Logic in Add Accessor

class MyClass
{
    private static object _lock = new object();
    private static bool _initialized;
    private static EventHandler _myEvent;
    public static event EventHandler MyEvent
    {
        add
        {
            lock(_lock)
            {
                EnsureInitialized();
                _myEvent += value;
            }
        }
        remove { lock(_lock) _myEvent -= value; }
    }
    private static void EnsureInitialized()
    {
        if (!_initialized)
        {
            ... // do initialization here
            _initialized = true;
        }
    }
    ...
}

注冊委托時可能需要執行的另一種自定義邏輯是權限要求。以 System.Diagnostics.EventLog 類為例 。EventLog 上的許多操作都是由 EventLogPermission 控制的,它通過 EventLogPermissionAccess 枚 舉(包括如 Administer、Write 和 None 這樣的成員)的指定來確定權限級別。EventLog 類公開 EntryWritten 公共事件,每當向日志寫入條目時就會引發該事件,但是,對事件日志中的新增條目進行 響應這一操作則需要具有 Administer(管理員)權限。何處需要管理員權限?沒有權限的代碼不可以向 事件注冊委托,因此,可在 add 存取器中實現此要求,如圖 5 所示。

Figure 5 Permission Demand in Add Accessor

class MyClass
{
    private EventHandler _myEvent;
    public event EventHandler MyEvent
    {
        add
        {
            new EventLogPermission(
                EventLogPermissionAccess.Administer, ".").Demand();
            _myEvent += value;
        }
        remove { _myEvent -= value; }
    }
}

System.Console 類利用自定義邏輯進行自定義初始化和提出權限要求。Console 公開靜態 CancelKeyPress 事件,當在控制台中使用 Ctrl+C 或 Ctrl+Break 組合鍵時即會引發該事件。通常,這 些操作會終止進程,但是如果 ConsoleCancelEventArgs 委托向該事件進行了注冊,則該委托可以通過 ConsoleCancelEventArgs.Cancel 屬性覆蓋這一終止處理。為了執行此攔截,Console 必須注冊一個處理 程序以使用 SetConsoleCtrlHandler 捕獲來自 Kernel32.dll 的信號,該事件的 add 存取器是執行此操 作的合適位置(remove 存取器包含與注銷該處理程序相對應的邏輯)。此外,CancelKeyPress 需要 UIPermissionWindow.SafeTopLevelWindows 權限,因此,在該事件的兩種存取器中都要對其提出權限要 求。

有一個(高級開發人員發現的)不太為人所知的 add 存取器自定義邏輯的使用方式,該方式可用於在 受約束的執行區域或 CER 內引發的事件。(有關 CER 的詳細信息,請參閱 2005 年 10 月的 MSDN雜志 中刊載的我的文章。)此處的基本思想是,對於諸如內存不足之類情況導致的異步異常情況,它會阻止標 准備份代碼的正確執行,中斷正常的代碼流,從而使應用程序變得不穩定。為了防止出現這類情況,可以 對代碼進行 JIT 編譯並預先准備,這樣就有望使這類情況在代碼運行前或運行後發生。

現在來考慮在 CER 內引發的事件,此處您希望避免出現所有這些危險情況。您要提前准備要在 CER 中執行的所有代碼,但是,如果要引發一個任何委托都可以注冊到的事件,則實際上,可能會在 CER 內 運行非預先准備的代碼。這正是 AppDomain 類上的幾個事件所遇到的情況。

例如,AppDomain.ProcessExit 是從 CER 內部引發的,因此所有向其注冊的委托都應預先准備。怎樣 實現呢?通過向事件的 add 存取器添加代碼,在向該事件注冊提供的委托前,通過 add 存取器准備委托 ,如圖 6 所示。

Figure 6 Delegate Preparation in Add Accessor

class MyClass
{
    private EventHandler _myEvent;
    public event EventHandler MyEvent
    {
        add
        {
            System.Runtime.CompilerServices.
                RuntimeHelpers.PrepareDelegate(value);
            _myEvent += value;
        }
        remove { _myEvent -= value; }
    }
}

這只是您為什麼要為事件提供自己的 add 存取器和 remove 存取器的原因之一。大部分時間,由編譯 器生成的存儲存取器是有效的。但是,有時候您需要更多的控制,使其成為隨手可及的方便功能。

將您想向 Stephen 詢問的問題和提出的意見發送至 [email protected].

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved