把事件當作對象進行傳遞

作者: Jeffrey Zhao  來源: 博客園  發布時間: 2009-09-08 09:55  閱讀: 1746 次  推薦: 0   原文鏈接   [收藏]  

  最近在琢磨一些事情,和API設計有關。API設計在很多時候是和語言特性有關的,因此如Java這樣的語言,在API設計時會處處受到壓抑。而C#就能夠出現如MoqFluent NHIbernate這樣的項目。同樣,F#能夠開發出FsTest,Scala號稱Scalable Language,都是依靠著豐富的語言特性。不過,最近在使用C#的時候鼻子上也碰了一點灰,這是因為我發現“事件”這個東西沒法作為對象進行傳遞。

public class Program
{
    public event EventHandler Submit;
}

  我們如果要為這個事件添加處理函數自然只要:

var myClass = new MyClass();
myClass.Submit += (sender, eventArgs) => Console.WriteLine(sender);

 

  但是,如果我想寫一個“統一添加”的輔助函數,例如可以這樣調用:

RegisterHandlers(myClass.Submit);

 

  就會發現——做不到。雖然,如果我們提供這樣的RegisterHandlers方法的實現:

class Program
{
    public event EventHandler Submit;

    static void RegisterHandlers(EventHandler ev)
    {
        ev += (sender, eventArgs) => Console.WriteLine("sender");
    }

    static void Main(string[] args)
    {
        Program p = new Program();
        RegisterHandlers(p.Submit);

        p.Submit("Hello World", EventArgs.Empty);
    }
}

  這是可以編譯通過的,似乎……應該也過得去。但是實際執行的時候就會發現,p.Submit事件在觸發的時候依然會拋出NullReferenceException異常(為什么?)。因此,我們必須選擇另外一種方式。

  我們知道,雖說是一個事件,但是在注冊和移除處理函數的時候,實際上都是在調用add方法和remove方法。例如這句代碼:

myClass.Submit += (sender, eventArgs) => Console.WriteLine(sender);

  和下面的代碼其實是“等價”的:

 

myClass.add_Submit((sender, eventArgs) => Console.WriteLine(sender));

  “等價”打上引號是因為add_Submit這行代碼其實無法編譯通過,我只是用來表示一個含義。但是這意味著,我們可以通過反射來調用add方法和remove方法。因此,我編寫了這樣的一個類:

public class Event
{
    public Event(Expression<Func> eventExpr)
    {
        ...
    }

    private object m_instance;
    private MethodInfo m_addMethod;
    private MethodInfo m_removeMethod;

    public Event AddHandler(T handler)
    {
        this.m_addMethod.Invoke(this.m_instance, new object[] { handler });
        return this;
    }

    public Event RemoveHandler(T handler)
    {
        this.m_removeMethod.Invoke(this.m_instance, new object[] { handler });
        return this;
    }
}

 

  于是,我可以設法把一個事件封裝為一個對象:

class Program
{
    public event EventHandler Submit;

    static void Main(string[] args)
    {
        Program p = new Program();
        var ev = new Event<EventHandler>(() => p.Submit);
        ev.AddHandler((sender, eventArgs) => Console.WriteLine(sender));

        p.Submit("Hello World", EventArgs.Empty);
    }
}

 

  那么Event類的構造函數該怎么寫呢?不過是解析表達式樹而已:

public Event(Expression<Func> eventExpr)
{
    var memberExpr = eventExpr.Body as MemberExpression;
    this.m_instance = memberExpr.Expression == null ? null :
        Expression.Lambda<Func<object>>(memberExpr.Expression).Compile()();

    var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.InvokeMethod |
        (this.m_instance == null ? BindingFlags.Static : BindingFlags.Instance);

    var member = memberExpr.Member;
    this.m_addMethod = member.DeclaringType.GetMethod("add_" + member.Name, bindingFlags);
    this.m_removeMethod = member.DeclaringType.GetMethod("remove_" + member.Name, bindingFlags);
}

 

  對于() => p.Submit這樣的代碼來說,它是一個MemberExpression,我們可以通過MemberExpression的屬性來說的p的實例。然后,根據Submit屬性的Member的Name便可以得出它的add或remove方法。其中需要再判斷這是一個實例事件還是一個靜態事件就可以了。總體來說,代碼比較簡單。當然,在實際運用中會要求在不合法的情況下拋出合適的異常。此外,如果您對性能有要求,也可以使用FastLambdaFastReflectionLib來提高性能。

  為了方便使用,我還為Event類重載了+和-兩個操作符,以及一個EventFactory類:

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

public class Event
{
    ...

    public static Event operator +(Event ev, T handler)
    {
        return ev.AddHandler(handler);
    }

    public static Event operator -(Event ev, T handler)
    {
        return ev.RemoveHandler(handler);
    }
}

 

  EventFactory類的Create方法可以避免顯式地提供T類型,而+和-操作符的目的便是在添加和刪除事件處理函數的時候“更像那么一回事”。于是現在我們便可以寫這樣的代碼:

class Program
{
    public event EventHandler Submit;

    static void Main(string[] args)
    {
        Program p = new Program();
        var ev = EventFactory.Create(() => p.Submit);
        ev += (sender, eventArgs) => Console.WriteLine(sender);

        p.Submit("Hello World", EventArgs.Empty);

        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();
    }
}

 

  既然有了Event對象,我們便可以把它作為參數傳遞給其他方法,然后在其他的方法中添加或刪除事件處理函數。

  是不是挺美妙的?您也來下載完整代碼試試看吧,而且說不定……您還能發現這個方法里的一個陷阱。我承認,其實這個解決方案會遇見C#的一個問題,它糊弄了我,也糊弄了大家……

0
0
 
標簽:事件 對象
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()