Hello World
Spiga

基于DelegateEvent创建第一个IEvent对象

2009-09-11 13:47 by 老赵, 12563 visits

继续和“事件即对象”打交道。我们之前提到过两个“趣味编程”:DelegateEventFunctional Reactive Programming,现在我们在它们两者之间架起一座桥梁。也就是说,我们要从一个DelegateEvent<TDelegate>对象创建一个IEvent<TEventArgs>对象出来,其实也就是实现这样一个接口:

public interface IEvent<TEventArgs>
{
    void Add(Action<TEventArgs> callback);
}

public class DelegateEvent<TDelegate>
{
    public IEvent<TEventArgs> Wrap<TEventArgs>()
        where TEventArgs : EventArgs
    {
        ...
    }

    // other members...
}

比较可惜的是,即使得到了一个DelegateEvent<TDelegate>对象,Wrap方法还是需要显式提供TEventArgs参数。因为C#没有一个机制可以静态推断出一个委托的第二个参数是什么类型。而且,事实上TDelegate并不是一个适合事件的委托类型。这是因为,作为.NET中用于事件的委托,是需要满足一定要求的:

  1. 没有返回值(返回void)。
  2. 拥有两个参数。
  3. 第一个参数为object类型。
  4. 第二个参数为System.EventArgs的子类。

于是,我们现在的任务,便是基于目前的DelegateEvent,创建一个IEvent类型出来。由于它是独立为DelegateEvent服务的,我将其定义为DelegateEvent的内部类:

public class DelegateEvent<TDelegate>
{
    public IEvent<TEventArgs> Wrap<TEventArgs>()
        where TEventArgs : EventArgs
    {
        return new NativeEvent<TEventArgs>(this);
    }

    // other members...

    private class NativeEvent<TEventArgs> : IEvent<TEventArgs>
        where TEventArgs : EventArgs
    {
        public NativeEvent(DelegateEvent<TDelegate> delegateEvent)
        {
            ...
        }

        // other members...
    }
}

既然C#无法帮助我们获取TDelegate的第二个参数的类型,那么我们的第一步便是“提取”出这个Type对象:

private class NativeEvent<TEventArgs> : IEvent<TEventArgs>
    where TEventArgs : EventArgs
{
    private DelegateEvent<TDelegate> m_delegateEvent;
    private Type m_eventArgsType;

    public NativeEvent(DelegateEvent<TDelegate> delegateEvent)
    {
        this.m_delegateEvent = delegateEvent;
        this.m_eventArgsType = this.GetEventArgsType();
    }

    private Type GetEventArgsType()
    {
        var invokeMethod = typeof(TDelegate).GetMethod("Invoke");
        if (invokeMethod.ReturnType != typeof(void))
        {
            throw new InvalidOperationException("Invalid delegate type of event.");
        }

        var parameters = invokeMethod.GetParameters();
        if (parameters.Length != 2 ||
            parameters[0].ParameterType != typeof(object) ||
            !typeof(TEventArgs).IsAssignableFrom(parameters[1].ParameterType))
        {
            throw new InvalidOperationException("Invalid delegate type of event.");
        }

        return parameters[1].ParameterType;
    }

    // other members...
}

在GetEventArgsType方法中,我们首先通过TDelegate的类型获取它Invoke方法——这便是委托的签名信息。由于DelegateEvent已经帮我们确认TDelegate一定是一个委托,因此invokeMethod对象一定不为null。于是我们开始全方位的检查,如果遇到了以下任意一种情况,则抛出异常:

  1. 返回类型不为void。
  2. 参数数量不为2。
  3. 第一个参数不为object类型。
  4. 第二个参数是否与TEventArgs兼容。

除了最后一点要求,其他都与之前的“标准”一一对应。“标准”中的第4点已经由泛型参数TEventArgs的约束保证了,而我们检查的其实是TEventArgs与第二个参数类型的“兼容性”。换句话说,我们的实现可以允许各种转换方式,如父子继承,隐式转换等等。

NativeEvent<TEventArgs>类型基于DelegateEvent<TDelegate>,自然需要为DelegateEvent添加一个事件处理函数,然后再事件触发时,利用第二个参数再去触发自身的每个回调方法。因此,我们还要为NativeEvent添加这些代码:

private class NativeEvent<TEventArgs> : IEvent<TEventArgs>
    where TEventArgs : EventArgs
{
    private List<Action<TEventArgs>> m_callbacks = new List<Action<TEventArgs>>();

    public void Add(Action<TEventArgs> callback)
    {
        if (this.m_callbacks.Count == 0)
        {
            this.RegisterEventHandler();
        }

        this.m_callbacks.Add(callback);
    }

    private void RegisterEventHandler()
    {
        ...
    }

    private void FireEvent(TEventArgs args)
    {
        foreach (var callback in this.m_callbacks) callback(args);
    }

    // other members...
}

当外界使用Add方法向NativeEvent添加回调方法时,会将回调方法保存起来。需要注意的是,只有当外界“第一次”调用Add方法时,NatvieEvent才会去“监听”DelegateEvent对象,而监听这个行为是由RegisterEventHandler负责的。它的实现是NativeEvent的又一关键。

既然是要监听DelegateEvent,自然是要准备一个委托对象并使用AddHandler方法交给DelegateEvent。但是,我们现在只知道TDelegate类型,但是无法真正确定它的签名,因此,我们无法在编译期准备好一个方法来创建TDelegate对象,我们能做的就是在运行时动态生成一个委托。那么这个委托的形式应该是什么样的呢?这倒容易:

(sender, args) => this.FireEvent((TEventArgs)args)

这是一个使用Lambda表达式生成的匿名方法,我们需要动态生成的委托便是这个模样。于是剩下来的便交给表达式树吧:

private static MethodInfo s_fireEventMethod =
    typeof(NativeEvent<TEventArgs>).GetMethod("FireEvent",
        BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod);

private void RegisterEventHandler()
{
    // sender
    var senderExpr = Expression.Parameter(typeof(object), "sender");
    // eventArgs
    var eventArgsExpr = Expression.Parameter(this.m_eventArgsType, "args");
    // (TEventArgs)eventArgs
    var castExpr = typeof(TEventArgs) == this.m_eventArgsType ? eventArgsExpr :
        (Expression)Expression.Convert(eventArgsExpr, typeof(TEventArgs));
    // this
    var thisExpr = Expression.Constant(this);
    // this.FireEvent((TEventArgs)args)
    var bodyExpr = Expression.Call(thisExpr, s_fireEventMethod, castExpr);
    // (sender, args) => this.FireEvent((TEventArgs)args)
    var lambdaExpr = Expression.Lambda<TDelegate>(bodyExpr, senderExpr, eventArgsExpr);
    
    this.m_delegateEvent += lambdaExpr.Compile();
}

构造表达式的过程分以下几步走:

  1. 构造一个object类型的参数sender。
  2. 构造一个委托第二个参数(m_eventArgsType)类型的参数args。
  3. (如果TEventArgs与委托第二个参数类型不同,则)构造一个转化操作(TEventArgs)args。
  4. 构造一个常量this。
  5. 构造一个方法调用this.FireEvent((TEventArgs)args)。
  6. 将上述方法调用封装为一个Lambda表达式:(sender, args) => this.FireEvent((TEventArgs)args)。

Complie之后即大功告成。可见,生成一个表达式树也是非常直观的,您在构造一个表达式时候,也可以使用这种方法,一步一步地进行下去。

在使用时,我们可以通过这样的代码:

var de = new DelegateEvent<EventHandler>(...);
var e = de.Wrap<EventArgs>();
e.Add((eventArgs) => Console.WriteLine(eventArgs));

现在,您可以把它作为第一个IEvent对象,继续尝试Functional Reactive Programming了。

Creative Commons License

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

Add your comment

34 条回复

  1. hystar
    *.*.*.*
    链接

    hystar 2009-09-11 13:54:00

    沙发??老赵的沙发??不容易。。。
    ps:最近老赵的博客"新陈代谢“旺盛啊

  2. Selfocus
    *.*.*.*
    链接

    Selfocus 2009-09-11 13:54:00

    做个调查,看懂了的举左手,没看懂的举右手

  3. Selfocus
    *.*.*.*
    链接

    Selfocus 2009-09-11 14:04:00

    hystar:
    沙发??老赵的沙发??不容易。。。
    ps:最近老赵的博客"新陈代谢“旺盛啊


    不好意思,您来晚了,哈哈哈!

  4. 我有疑问[未注册用户]
    *.*.*.*
    链接

    我有疑问[未注册用户] 2009-09-11 14:35:00

    老赵:
    我们在解决实际问题中能用到这些东西么?我个人在做项目中很难碰到呀!

  5. lyf007[未注册用户]
    *.*.*.*
    链接

    lyf007[未注册用户] 2009-09-11 14:35:00

    举右手 努力学习中...

  6. 菜鸟113[未注册用户]
    *.*.*.*
    链接

    菜鸟113[未注册用户] 2009-09-11 14:36:00

    我们在做角色和资源权限判断的时候是在Application的事件里判断还是在页面load里判断呀?请指教!多谢

  7. GB[未注册用户]
    *.*.*.*
    链接

    GB[未注册用户] 2009-09-11 14:38:00

    赵兄呀,我来T馆的

    有人说呀,贴出牛X的代码,就够您老人家喝上好几壶的了。。。哈哈

  8. 老赵
    admin
    链接

    老赵 2009-09-11 14:43:00

    @我有疑问
    学习的成分大于实际的成分,学习先进思想,要用的话……我很少做UI方面的东西,所以使用不多。

  9. 老赵
    admin
    链接

    老赵 2009-09-11 14:46:00

    @菜鸟113
    哪儿合适放哪儿,根据具体情况来。
    不过一般不会放在Page的Load里,要放也在Page.PreInit里。

  10. 老赵
    admin
    链接

    老赵 2009-09-11 14:47:00

    @GB
    我怎么没听懂你再说什么……

  11. 非空
    *.*.*.*
    链接

    非空 2009-09-11 14:50:00

    我没看懂sender怎么传进去的

  12. 老赵
    admin
    链接

    老赵 2009-09-11 14:58:00

    @非空
    IEvent忽略sender。

  13. gjcn
    *.*.*.*
    链接

    gjcn 2009-09-11 16:38:00

    看来大哥最近很闲啊,更新速度那是相当的快啊

  14. 老赵
    admin
    链接

    老赵 2009-09-11 16:40:00

    @gjcn
    不闲,只是抓紧时间而已。

  15. 老赵救救园子里的兄弟吧[未注册用户]
    *.*.*.*
    链接

    老赵救救园子里的兄弟吧[未注册用户] 2009-09-11 16:44:00

    老赵救救园子里的兄弟吧!
    什么烂文章都发到首页来,叫我们怎么活啊!!!!!!!!

  16. 张磊。
    *.*.*.*
    链接

    张磊。 2009-09-11 16:44:00

    昨天就把这个地方完成了,接下来不知道怎么写Filter这些了,还要用ExpressionTree写逻辑吗

  17. 大唐电信123[未注册用户]
    *.*.*.*
    链接

    大唐电信123[未注册用户] 2009-09-11 16:46:00

    @Jeffrey Zhao
    老赵:我拜托你,让博客园封杀那个整天忽悠“权限系统”的那个无知的家伙把,我实在受不了了,博客园这块技术净土被他搞得乌烟瘴气,我拜托啦,你向博客园的管理员说说,此人一定要杀,否则你看看现在成了什么了,风气大坏呀,拜托拜托拜托!

  18. 老赵救救园子里的兄弟吧[未注册用户]
    *.*.*.*
    链接

    老赵救救园子里的兄弟吧[未注册用户] 2009-09-11 16:48:00

    大唐电信123:
    @Jeffrey Zhao
    老赵:我拜托你,让博客园封杀那个整天忽悠“权限系统”的那个无知的家伙把,我实在受不了了,博客园这块技术净土被他搞得乌烟瘴气,我拜托啦,你向博客园的管理员说说,此人一定要杀,否则你看看现在成了什么了,风气大坏呀,拜托拜托拜托!


    那个吉日嘎拉[权限设计] 简直让人恶心死了,有本事吹那是你牛,没本事吹让人恶心!!!!!!!!!!!

  19. 受不了了才[未注册用户]
    *.*.*.*
    链接

    受不了了才[未注册用户] 2009-09-11 16:54:00

    老赵救救园子里的兄弟吧:

    大唐电信123:
    @Jeffrey Zhao
    老赵:我拜托你,让博客园封杀那个整天忽悠“权限系统”的那个无知的家伙把,我实在受不了了,博客园这块技术净土被他搞得乌烟瘴气,我拜托啦,你向博客园的管理员说说,此人一定要杀,否则你看看现在成了什么了,风气大坏呀,拜托拜托拜托!


    那个吉日嘎拉[权限设计] 简直让人恶心死了,有本事吹那是你牛,没本事吹让人恶心!!!!!!!!!!!


    明眼人很清楚,那是个广告贴,但一些呆鸟乱捧,纯粹广告贴,无聊之至

  20. 老赵
    admin
    链接

    老赵 2009-09-11 16:54:00

    @张磊。
    不需要,接下来就是普通的编程。
    其实接下来才是关键,我是指更有意义的地方。
    因为我认为值得锻炼的应该是逻辑,而不是像现在这篇那样是“框架使用技巧”。

  21. 老赵
    admin
    链接

    老赵 2009-09-11 16:56:00

    @大唐电信123
    我倒觉得他没有在影响风气,不是骂他的人很多吗?
    真正影响风气的应该是那些道貌岸然,装B,被人当作牛人但传递浮躁之风的人。
    对于这种情况我毫不留情的,但是现在看上去都还好。

  22. 草羹
    *.*.*.*
    链接

    草羹 2009-09-11 16:58:00

    大佬,您老人家的更新速度太快了,不利于吸收啊!

  23. GB[未注册用户]
    *.*.*.*
    链接

    GB[未注册用户] 2009-09-11 17:01:00

    Jeffrey Zhao:
    @GB
    我怎么没听懂你再说什么……


    看来你太不关注吉日(JR)的那文章了,你去看看结尾的,哈哈

  24. Jeff Wong
    *.*.*.*
    链接

    Jeff Wong 2009-09-11 17:03:00

    Jeffrey Zhao:
    @大唐电信123
    我倒觉得他没有在影响风气,不是骂他的人很多吗?
    真正影响风气的应该是那些道貌岸然,装B,被人当作牛人但传递浮躁之风的人。
    对于这种情况我毫不留情的,但是现在看上去都还好。


    老赵火眼金睛,请举两个“道貌岸然,装B,被人当作牛人但传递浮躁之风的人”来说说,也让我们新手有鉴定的标准。首页有很多文章一上来就比你这里还热闹,进去一看,全是吵架的,氛围真的不好。

  25. 老赵
    admin
    链接

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

    @草羹
    我尽快写,有任何感想都记录下来。
    你可以慢慢看,不会过时的。

  26. 老赵
    admin
    链接

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

    @GB
    找不到啊,算了,hoho

  27. 老赵
    admin
    链接

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

    @Jeff Wong
    最近好像没有此类人出没。

  28. 小城故事
    *.*.*.*
    链接

    小城故事 2009-09-11 17:52:00

    难道我的差距越来越大了?发现首页的技术文章只有吉日和我自己的能看的懂。

  29. 请教个问题[未注册用户]
    *.*.*.*
    链接

    请教个问题[未注册用户] 2009-09-11 21:42:00

    老赵:
    可以在这里请教个问题吗?

    public void MyTest(Func<int> func)
    {
    int result = func();
    int add = result + 1;
    return result;
    }

    能不能在不判断result的情况下,让MyTest在执行完func的情况下不判断送result的值而不再继续执行下边的代码?也就是让方法在func中返回...

  30. 请教个问题[未注册用户]
    *.*.*.*
    链接

    请教个问题[未注册用户] 2009-09-11 21:44:00

    不好意思.手滑.不用return上边的方法.

  31. 老赵
    admin
    链接

    老赵 2009-09-11 23:20:00

    @请教个问题
    实在没理解你的意思。

  32. javascript[未注册用户]
    *.*.*.*
    链接

    javascript[未注册用户] 2009-09-12 00:41:00

    Jeffrey Zhao:
    @请教个问题
    实在没理解你的意思。


    他的意思是执行int result = func();一行时,在某种状况下直接导致MyTest的返回( 如func()返回 0 时立即终止MyTest,而不需要增加对result的判断代码)。这么不合理念头我替你回答他:func里抛出异常就是了,以代替result,反正你也不想再用它作判断。

  33. dada7357
    *.*.*.*
    链接

    dada7357 2009-09-13 00:40:00

    老赵,你现在的Webcast的MVC.net还继续不?不如开个新的WebCast来讲吓你近写的东西,东西出得太快,有好多都是一知半解,不利吸收,谢谢

  34. 老赵
    admin
    链接

    老赵 2009-09-13 01:10:00

    @dada7357
    我认为还是写文章比较靠谱,可以反复的看。
    WebCast无论是录制还是吸收成本都太高。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我