把事件当作对象进行传递
2009-09-07 17:20 Jeffrey Zhao 阅读(7492) 评论(89) 编辑 收藏 举报最近在琢磨一些事情,和API设计有关。API设计在很多时候是和语言特性有关的,因此如Java这样的语言,在API设计时会处处受到压抑。而C#就能够出现如Moq或Fluent 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方法。其中需要再判断这是一个实例事件还是一个静态事件就可以了。总体来说,代码比较简单。当然,在实际运用中会要求在不合法的情况下抛出合适的异常。此外,如果您对性能有要求,也可以使用FastLambda即FastReflectionLib来提高性能。
为了方便使用,我还为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#的一个问题,它糊弄了我,也糊弄了大家……