程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 趣味編程:將事件視為對象(參考答案)

趣味編程:將事件視為對象(參考答案)

編輯:關於.NET

這次我們是要編寫一個DelegateEvent<TDelegate>對象,提供它AddHandler和RemoveHandler的實現。事實上,在之前還有一篇文章中,我們搞了一個人模狗樣的構造方式,但是它往往不適合用於實際使用過程中。因此,其實DelegateEvent<TDelegate>最關鍵的地方還是各種不同的“構造方式”,使它可以用於各種情況。

方法一:直接提供添加刪除的實現

在之前的文章裡,已經有一些朋友提出了最簡單的做法,即直接提供AddHandler和RemoveHandler的實現,例如:

public class DelegateEvent<TDelegate>
{
   private Action<TDelegate> m_addHandler;
   private Action<TDelegate> m_removeHandler;

   public DelegateEvent(Action<TDelegate> add, Action<TDelegate> remove)
   {
     this.CheckDelegateType();

     if (add == null) throw new ArgumentNullException("add");
     if (remove == null) throw new ArgumentNullException("remove");

     this.m_addHandler = add;
     this.m_removeHandler = remove;
   }

   private void CheckDelegateType()
   {
     if (!typeof(Delegate).IsAssignableFrom(typeof(TDelegate)))
     {
       throw new ArgumentException("TDelegate must be an Delegate type.");
     }
   }

   public DelegateEvent<TDelegate> AddHandler(TDelegate handler)
   {
     this.m_addHandler(handler);
     return this;
   }

   public DelegateEvent<TDelegate> RemoveHandler(TDelegate handler)
   {
     this.RemoveHandler(handler);
     return this;
   }
}

用戶可以直接提供兩個委托,分別在AddHandler和RemoveHandler方法中使用。在構造函數中我們除了進行參數的非null判斷之外,還會進行委托類型的檢查。C#在泛型約束方面的遺憾很多,例如我們無法將一個泛型參數約束為一個Delegate類型的子類。因此,我們使用 CheckDelegateType來檢查類型是否兼容。

於是我們便可以這樣編寫代碼:

var myClass = new MyClass();
var de = new DelegateEvent<EventHandler>(
   h => myClass.MyEvent += h,
   h => myClass.MyEvent -= h); 

這個方法的缺點也是比較明顯的。那就是,我們無法限制add和remove兩個委托的實現是否正確,即時程序員提供了一些錯誤實現也只得老老實實地執行。例如,我們可能一不小心會在add操作裡也用上了+=,或者一不小心在remove操作中去刪除了另一個事件。而且,這樣的問題還比較難以發現。此外,這種做法需要些的代碼也比較多。一個優秀的API設計,應該讓開發人員易於使用,也盡可能避免出現不必要的麻煩——至少,盡早把錯誤匯報出來。

方法二:實例+事件名

提供一個對象實例,再給出一個事件名,也可以構造一個DelegateEvent對象:

public DelegateEvent(object obj, string eventName)
{
   this.CheckDelegateType();

   if (obj == null) throw new ArgumentNullException("obj");
   if (String.IsNullOrEmpty(eventName)) throw new ArgumentNullException("eventName");

   this.BindEvent(obj.GetType(), obj, eventName);
}

private void BindEvent(Type type, object obj, string eventName)
{
   var eventInfo = type.GetEvent(eventName,
     BindingFlags.Public | BindingFlags.NonPublic | 
     (obj == null ? BindingFlags.Static : BindingFlags.Instance));

   if (eventInfo == null)
   {
     throw new ArgumentException(
       String.Format("Event {0} is missing in {1}",
         eventName, type.FullName));
   }

   if (eventInfo.EventHandlerType != typeof(TDelegate))
   {
     throw new ArgumentException(
       String.Format("Type of event {0} in {1} is mismatched with {2}.",
         eventName, type.FullName, typeof(TDelegate).FullName));
   }

   this.m_addHandler = h => eventInfo.AddEventHandler(obj, (Delegate)(object)h);
   this.m_removeHandler = h => eventInfo.RemoveEventHandler(obj, (Delegate)(object)h);
}

得到了對象和事件名之後,自然要做的還是檢查參數是否為空,以及TDelegate類型是否為一個委托,接著便是用BindEvent方法來綁定m_addHandler和m_removeHandler兩個委托。在BindEvent方法中,主要使用了反射獲取事件的eventInfo對象。對eventInfo的檢查主要體現在兩方面,一是是否存在這個事件,二是這個事件的委托類型是否等於TDelegate。m_addHandler和 m_removeHandler委托的構造很簡單,不過由於編譯器不允許將一個不定類型TDelegate轉化為Delegate,因此我們必須先將其轉為object,再轉化為Delegate並添加到eventInfo中。

這個構造函數的使用方法如下:

var myClass = new MyClass();
var de = new DelegateEvent<EventHandler>(myClass, "MyEvent");

這個做法的缺點在於事件使用字符串來表示,這意味著錯誤只有在運行時才能改變。此外,如果您想通過重構來修改MyEvent事件的名稱,編輯器也是無法為您修改這裡的字符串的。因此,我們一直強調“強類型”,一個重要的目的便是獲得靜態檢查及重構支持。

此外,這個做法還無法幫定靜態事件。因此,我們還要努力。

方法三:類型+事件名

既然是靜態事件,那麼綁定的就不是對象的事件,而是類型的事件。因此,第三種做法是提供一個表示類型的對象,以及一個事件名:

public DelegateEvent(Type type, string eventName)
{
   this.CheckDelegateType();

   if (type == null) throw new ArgumentNullException("type");
   if (String.IsNullOrEmpty(eventName)) throw new ArgumentNullException("eventName");

   this.BindEvent(type, null, eventName);
}

沒錯,就是這麼簡單。許多邏輯已經包含在BindEvent方法中。它的使用方法如下:

var de = new DelegateEvent<EventHandler>(typeof(MyClass), "MyStaticEvent");

這裡的缺點與之前相同,無法獲得靜態檢查和重構支持。這似乎是沒有辦法的,即使在Reactive Framework中,微軟朋友們也是使用字符串來綁定一個事件。

方法四:使用表達式樹指定事件

這就是文章一開始提到的“人模狗樣”的做法。他雖然有很大限制,但並不是一無是處。因此,也把它算作是一個方法吧:

public DelegateEvent(Expression<Func<TDelegate>> eventExpr)
{
   this.CheckDelegateType();

   // () => obj.EventName 
   if (eventExpr == null) throw new ArgumentNullException("eventExpr");

   // obj.EventName 
   var memberExpr = eventExpr.Body as MemberExpression;
   if (memberExpr == null)
   {
     throw new ArgumentNullException("eventExpr", "Not an event.");
   }

   object instance = null;
   // obj 
   if (memberExpr.Expression != null)
   {
     try
     {
       // () => obj 
       var instanceExpr = Expression.Lambda<Func<object>>(memberExpr.Expression);
       instance = instanceExpr.Compile().Invoke();
     }
     catch (Exception ex)
     {
       throw new ArgumentNullException("eventExpr is not an event", ex);
     }
   }

   this.BindEvent(memberExpr.Member.DeclaringType, instance, memberExpr.Member.Name);
}

經過了幾次表達式樹的組裝和解析,不知道您是否還認為這是一個難以接觸的話題呢?從注釋中可以發現,其實它的每一步操作都是非常清晰的,可以方便而有條理地提取表達式中的信息。為了配合C#編譯器的類型推斷功能,我們還可以補充一個輔助方法來構造DelegateEvent的對象:

public static class EventFactory
{
   public static DelegateEvent<T> Create<T>(Expression<Func<T>> eventExpr)
   {
     return new DelegateEvent<T>(eventExpr);
   }
}

於是,我們便可以這樣使用:

class Program 
{
   public event EventHandler MyEvent;
   public static event EventHandler MyStaticEvent;

   static void Main(string[] args)
   {
     var sde = EventFactory.Create(() => MyStaticEvent);

     var p = new Program();
     var de = EventFactory.Create(() => p.MyEvent);
   }
}

這個做法的優勢是易於編程,可以享受靜態檢查和重構等“福利”。但是,它的限制也已經討論過了,那就是“只能用於”定義事件的類中。也就是說,如果上面的代碼離開了Program的Main方法就無法編譯通過了。因此,這個方法只適用於由定義事件的類“親自”暴露出DelegateEvent對象的場景。如果您處於這個場景之下,那麼它幾乎就是您最好的選擇了。

總結

這就是我的參考答案,不難,但似乎也不是可以一蹴而就的。這也是我搞“趣味編程”的目的,我希望可以把每個題目的解決方案集中在一小個范圍內,將其各個方便挖掘出來,即使它們每個都很簡單。例如,您可能很容易就能想到了第一種方法,但是您是否也准備了適合其它使用場景下的構造方式?如果您編寫了各種構造方式,那麼是否把異常情況都判斷了呢?如果您判斷了異常情況,是否提供了輔助的API來簡化開發呢(如上面的EventFactory)?

這些不是“茴字有多少種寫法”的問題,這是在考察一個人是否考慮周全。據我觀察,“茴字有幾種寫法”很多情況下都已經成為一些朋友為自己思考不周,或了解不深的進行開脫的理由了。只可惜,例如某個方法有幾個異常方面,列舉跨頁面傳遞數據有幾種方法,C#中的成員有哪些修飾符等等,都是某些著名大公司考察應聘者的題目。

算了明說吧,就是微軟,而且就是我親自遇到的面試題。

文章來源:http://www.cnblogs.com/JeffreyZhao/archive/2009/09/17/on-event-as-object-practice-answer.html

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