代码改变世界

把事件当作对象进行传递

2009-09-07 17:20  Jeffrey Zhao  阅读(...)  评论(... 编辑 收藏

最近在琢磨一些事情,和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<T>
{
    public Event(Expression<Func<T>> eventExpr)
    {
        ...
    }

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

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

    public Event<T> 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<T>> 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<T> Create<T>(Expression<Func<T>> eventExpr)
    {
        return new Event<T>(eventExpr);
    }
}

public class Event<T>
{
    ...

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

    public static Event<T> operator -(Event<T> 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#的一个问题,它糊弄了我,也糊弄了大家……