Hello World
Spiga

趣味编程:将事件视为对象(参考答案)

2009-09-17 09:48 by 老赵, 12974 visits

题目在此。这次我们是要编写一个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://gist.github.com/188304

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

23 条回复

  1. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-09-17 09:54:00

    BIG SOFA!
    在OO的世界里,一切都是为了对象而服务。
    方法通过委托是可以扩展对象的行为,那么事件能够自由传递是为了什么呢?是不是两者有些重复的地方呢?

  2. 浪潮
    *.*.*.*
    链接

    浪潮 2009-09-17 09:56:00

    second

  3. 淘者天下2
    *.*.*.*
    链接

    淘者天下2 2009-09-17 10:18:00

    楼主真逗,还有参考答案,让人想起一些课本,总在后面有参考答案

  4. 老赵
    admin
    链接

    老赵 2009-09-17 10:21:00

    @淘者天下2
    其实我认为学编程最好的方法就是像中学做数学题一样,去图书馆里埋头写。

  5. 火星人.NET
    *.*.*.*
    链接

    火星人.NET 2009-09-17 10:55:00

    老赵写的这个很好用啊,多谢

  6. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-09-17 10:56:00

    Jeffrey Zhao:
    @淘者天下2
    其实我认为学编程最好的方法就是像中学做数学题一样,去图书馆里埋头写。


    SICP很适合这样读 精读 做习题
    忽然发现好书都是有习题的 TC++PL也有习题......

  7. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-09-17 10:59:00

    我觉得从语言设计的角度 event不能被引用、传递是语言设计中的一个硬性限制 所以我觉得强行这样做是不合适的

  8. 老赵
    admin
    链接

    老赵 2009-09-17 11:06:00

    @winter-cn
    把事件变成对象是一些编程模式的基础,这点是必须的,比如我文章里举出的Reactive Programming。
    “事件”其实是一种概念,而.NET中的事件只是.NET中规定的“标准”实现方式而已。
    如果你说“事件”不应该被强行暴露,那么可能就要绕一下,例如释放一个普通的回调形式出来。
    但是这个其实是一回事情了,我认为反而把标准事件对象暴露出来比较直接。

  9. feilng
    *.*.*.*
    链接

    feilng 2009-09-17 11:29:00

    我有些不明白,我们的目的是把事件当作对象,方便独立使用和传递
    这样就不必在对象实例上对事件进行操作。

    事件本身是个信号,我们所说的事件对象,就是管理了事件触发及其EventHandler的一个封装。达到的效果类似于QT中的Signal、Slot机制,我总觉得C#应该有更直观的实现方式。

  10. 老赵
    admin
    链接

    老赵 2009-09-17 11:32:00

    我们的目的是把事件当作对象,方便独立使用和传递
    这样就不必在对象实例上对事件进行操作。
    我们所说的事件对象,就是管理了事件触发及其EventHandler的一个封装。


    你说的没错,这就是DelegateEvent目的。
    其实你从DelegateEvent类型外面看起来是很简单的,复杂的是你看到的内部实现。
    俗话说,每一个优雅的接口背后都有丑陋的实现,这就是设计良好API的目的啊,呵呵。

  11. feilng
    *.*.*.*
    链接

    feilng 2009-09-17 11:35:00

    C#中的Delegate和MulticastDelegate正是这个机制的基本载体,但是.net中这2个类是编译器内部使用的,半开放的。

  12. feilng
    *.*.*.*
    链接

    feilng 2009-09-17 11:42:00

    我是说直接开放 Delegate和MulticastDelegate,让我们能够更加直接的实现这个目的,从实现上来说就是维护一个类型安全的函数指针列表,而该列表可以被安全的操作(AddHandlerRemoveHandler),该列表可以被安全独立的访问,当然也可以触发,而实际上当我们定义事件之后,这个事件Handler列表就存在了,所以我不喜欢再让我们通过事件字符串名字和反射机制来访问这个,他真的很臭

  13. feilng
    *.*.*.*
    链接

    feilng 2009-09-17 11:47:00

    C#语言对事件的实现应该更彻底,直接把事件作为对象对待,
    这样可以定义类成员事件就是定义一个对象成员,和定义一个IList成员没什么两样,我可以拿这个IList到任何地方操作

  14. feilng
    *.*.*.*
    链接

    feilng 2009-09-17 11:48:00

    对于属性,事件这种东西,编译器应该只提供便利,和安全性,其他不要限制

  15. 老赵
    admin
    链接

    老赵 2009-09-17 11:51:00

    @feilng
    嗯嗯,可惜C#有限制,如F#就会“自动转化”。

  16. feilng
    *.*.*.*
    链接

    feilng 2009-09-17 12:00:00

    说不定C#5.0就支持了,你不提合理要求5.0就还不支持,嘿嘿

  17. feilng
    *.*.*.*
    链接

    feilng 2009-09-17 12:13:00

    我最近有些胡乱的空想,我发现这些年“代码即数据”“把事件视为对象”,函数式编程,动态语言C#4.0 Compiler as a Service这些需求,这些在人们强烈需求驱动下才出现的特性,其实都指向一个大目标“自我反馈”,必须能够把由基本类型构造实现的任何东西看作基本类型,必须能够把代码真正当作数据,代码要和数据一样可以实现真正意义的流通,代码可以被其他代码当作数据操作,只有这样的一致性才能实现“智能”。

  18. 老赵
    admin
    链接

    老赵 2009-09-17 12:29:00

    @winter-cn
    所以我喜欢看教材,因为有习题,呵呵。

  19. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-09-17 12:38:00

    Jeffrey Zhao:
    @winter-cn
    把事件变成对象是一些编程模式的基础,这点是必须的,比如我文章里举出的Reactive Programming。
    “事件”其实是一种概念,而.NET中的事件只是.NET中规定的“标准”实现方式而已。
    如果你说“事件”不应该被强行暴露,那么可能就要绕一下,例如释放一个普通的回调形式出来。
    但是这个其实是一回事情了,我认为反而把标准事件对象暴露出来比较直接。


    你这么说当然没错 但是 C#设计特地限制了事件 其背后还是有很多考量的
    你认为把事件变成对象是Reactive Programming必需的,这没有错,但是你是否考虑过,你想要的事件是否就是event呢?

  20. 老赵
    admin
    链接

    老赵 2009-09-17 13:05:00

    @winter-cn
    就像我刚才说的,我要的是“事件”的概念,并不一定是要.NET标准中制定的event。
    我这么做的目的是直接使用.NET标准中制定的event,而不需要用其他绕弯的方法来做。
    例如,F#就会把.net中的event,变成它的IEvent对象,我只是做了F#已经自动完成的东西而已。
    此外,就如我文章里提到,微软在Silverlight里加入的Reactive Framework,用的也是类似的做法来封装事件。
    原因就是,既然已经有event了,为什么不直接把它暴露出来呢?我还有F#还有Reactive Framework是一个思路的。

  21. 老赵
    admin
    链接

    老赵 2009-09-17 13:06:00

    @winter-cn
    至于C#的设计考量,我比较同意上面feilng的观点,也就是说,我认为这是C#的缺陷,它应该提供一种内置的暴露方式,像F#一样。

  22. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-09-17 13:08:00

    Jeffrey Zhao:
    @winter-cn
    就像我刚才说的,我要的是“事件”的概念,并不一定是要.NET标准中制定的event。
    我这么做的目的是直接使用.NET标准中制定的event,而不需要用其他绕弯的方法来做。
    例如,F#就会把.net中的event,变成它的IEvent对象,我只是做了F#已经自动完成的东西而已。
    此外,就如我文章里提到,微软在Silverlight里加入的Reactive Framework,用的也是类似的做法来封装事件。
    原因就是,既然已经有event了,为什么不直接把它暴露出来呢?我还有F#还有Reactive Framework是一个思路的。


    我觉得不如直接使用delegate

  23. 老赵
    admin
    链接

    老赵 2009-09-17 13:15:00

    @winter-cn
    那就要在事件外另开一个delegate做委托了,不直接。
    而且,我也尝试过其他方法,发现还是必须创建一个类做委托,你可以试一下,是否可以设计出良好的API。
    我考虑最多的还是API。

    而且概念上,一个Button本来就有一个Button事件,如果封装成一个对象,大家也就知道它就是一个Button事件的对象。
    否则绕啊绕的……

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我