Hello World
Spiga

把事件当作对象进行传递

2009-09-07 17:20 by 老赵, 6227 visits

最近在琢磨一些事情,和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#的一个问题,它糊弄了我,也糊弄了大家……

Creative Commons License

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

Add your comment

89 条回复

  1. Galactica
    *.*.*.*
    链接

    Galactica 2009-09-07 17:27:00

    我被你糊弄了.

  2. 李永京
    *.*.*.*
    链接

    李永京 2009-09-07 17:27:00

    CSLA.NET更猛,还有比他更猛的

  3. Feng Lang[未注册用户]
    *.*.*.*
    链接

    Feng Lang[未注册用户] 2009-09-07 17:29:00

    老ZHAO的文章含金量真高

  4. 老赵
    admin
    链接

    老赵 2009-09-07 17:31:00

    李永京:CSLA.NET更猛,还有比他更猛的


    听不懂,啥意思?

  5. 老赵
    admin
    链接

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

    Galactica:我被你糊弄了.


    明天,明天给兄弟们一个答复,呵呵。

  6. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-07 17:33:00

    github中的代码粘贴之后,不是格式化的代码,需要手工换行……很费劲

  7. 老赵
    admin
    链接

    老赵 2009-09-07 17:35:00

    Feng Lang:老ZHAO的文章含金量真高


    含金量……
    我只能保证一点,这些全部不是“游戏”,都是我在实际生产过程中遇到的问题。

  8. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-07 17:36:00

    重载+和-?这恐怕不符合这两个运算符的语义吧。不过C#不准重载+=和-=,哎。。。

  9. 老赵
    admin
    链接

    老赵 2009-09-07 17:36:00

    @麒麟.NET
    你是不是没有从“raw”中复制的,而是直接复制带有格式的代码?

  10. virus
    *.*.*.*
    链接

    virus 2009-09-07 17:37:00

    老赵最近好像一直在打压JAVA

    API设计在很多时候是和语言特性有关的,因此如Java这样的语言,在API设计时会处处受到压抑。

    如果是这样的话,那java在银行、电信等重要部门的应用时如何开发的呢,他们的开发者自己不要设计大量的API吗,要是设计的时候都是感到压抑,他们会一直的用java吗,java会有那么大的市场吗,会在这些领域根深蒂固吗,至少根深蒂固了这么多年了,以后的很多年也是很难改变的,为什么呢

    当然了,我还是一个初学者,java还没有开发过项目,就是自己学习了一些,说话也没有准的,只是自己的一点想法吧

    一个乱说的人,希望大家原谅

  11. virus
    *.*.*.*
    链接

    virus 2009-09-07 17:38:00

    不过本人还是极为佩服老赵,真的希望自己可以达到老赵的一半就知足了,哈哈

  12. 老赵
    admin
    链接

    老赵 2009-09-07 17:38:00

    @Ivony...
    我觉得“加上一个处理函数”语义挺不错的阿。
    +只是一个操作符,字符串还把它当作一个“连接操作”来用呢。

  13. 老赵
    admin
    链接

    老赵 2009-09-07 17:40:00

    @virus
    你说错了,我是在打压“Java语言”,不是在打压“Java”。
    我说的没错,用java设计API的时候是挺压抑的(亲身体会)。
    只是没办法,当时如果想用Java平台,当时只有Java一种语言。
    而现在,Java平台上终于出现Scala,Groovy,Clojure语言了。
    我可是Scala的粉丝,相比F#我甚至更喜欢Scala。

  14. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-07 17:40:00

    @Jeffrey Zhao
    啊哦,我土了……

  15. 近近
    *.*.*.*
    链接

    近近 2009-09-07 17:44:00


    virus:不过本人还是极为佩服老赵,真的希望自己可以达到老赵的一半就知足了,哈哈


    这个好像要有点天分才行啊,我也是希望成为比多数人强点的那种啊,可惜还是那么菜。

  16. Galactica
    *.*.*.*
    链接

    Galactica 2009-09-07 18:11:00

    class Program
    {
    public event EventHandler Submit;

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

    static void Main(string[] args)
    {
    Program p = new Program();
    RegisterHandlers(ref p.Submit);
    p.Submit("Hello World", EventArgs.Empty);

    //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();
    }
    }

    帮老赵把那个碰了鼻子灰的问题修改了下.

  17. Rain.Shan
    *.*.*.*
    链接

    Rain.Shan 2009-09-07 18:20:00

    强烈建议 在每篇 文章的下方加 一行链接“下一篇” 和 “上一篇”

    如果是从别的地方搜索过来的,然后文章又是分开写的,找起来,很费劲。

    我记得以前有的啊,怎么现在没了? 难道我记错了?

    支持老赵,虽然很多看不懂,但是也要硬着头皮看完。

    阿弥陀佛,本人基督教的。

  18. 老赵
    admin
    链接

    老赵 2009-09-07 18:24:00

    @Rain.Shan
    从来没有过上一片下一片的,不过如果是有关联的文章,我自己会建立好链接的。

  19. Galactica
    *.*.*.*
    链接

    Galactica 2009-09-07 18:24:00

    Jeffrey Zhao:
    @Galactica
    你修改成这样的作用是什么呢?还是不行的……
    我说碰一鼻子灰,其实这篇文章结束时还没有解决掉,呵呵。



    代码贴错了,你再看下,我就是改了下static void RegisterHandlers(ref EventHandler ev)函数,改为 ref 传 EventHandler,这样p.Submit就不为null了.

  20. 老赵
    admin
    链接

    老赵 2009-09-07 18:26:00

    @Galactica
    这个改得差不多,呵呵。

  21. 老赵
    admin
    链接

    老赵 2009-09-07 18:35:00

    @RednaxelaFX
    呀,被发现了……嘿嘿。

  22. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-07 18:35:00

    @Jeffrey Zhao
    嗯老赵大忽悠……测试代码把Main写在别的类里嘛,那样就不会那么大忽悠了 XDD
    之前那篇是已经留意到了这个问题的……

  23. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-07 18:36:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    @RednaxelaFX
    呀,被发现了……


    嗯啊,刚看到() => p.Submit我就忍不住要跳起来了……

  24. AlexLiu
    *.*.*.*
    链接

    AlexLiu 2009-09-07 18:37:00

    lz要做头把交椅了

  25. 老赵
    admin
    链接

    老赵 2009-09-07 18:40:00

    @RednaxelaFX
    所以,C#还是挺不爽的。不过,其实这种方式也挺不错的:
    new DelegateEvent<EventHandler>(h => p.Submit += h, h => p.Submit -= h);

  26. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-07 18:41:00

    @Ivony...

    Ivony...:重载+和-?这恐怕不符合这两个运算符的语义吧。不过C#不准重载+=和-=,哎。。。


    其实也没错吧,因为委托的+本来就是对Delegate.Combine()的糖

  27. 老赵
    admin
    链接

    老赵 2009-09-07 18:42:00

    @RednaxelaFX
    C#的糖还不够多……

  28. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-07 18:48:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    @RednaxelaFX
    所以,C#还是挺不爽的。不过,其实这种方式也挺不错的:
    new DelegateEvent<EventHandler>(h => p.Submit += h, h => p.Submit -= h);


    这俩lambda的类型是什么?我试过这样写但是C#编译器不肯把它解糖之后再生成ETv1,所以之前我才推了一下抱怨这个

  29. 老赵
    admin
    链接

    老赵 2009-09-07 18:50:00

    @RednaxelaFX
    这……你一定把问题想太复杂了,这个东西一点技术含量也没有:

    public DelegateEvent(Action<T> add, Action<T> remove)
    {
        this.m_addHandler = add;
        this.m_removeHandler = remove;
    }

  30. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-07 18:55:00

    @Jeffrey Zhao
    orz
    嗯我知道为什么了,我想的确实复杂一些。我想像中的更“纯洁”一些,更“干”一些。不过试着去实现的时候为了弄到事件的名字弄了半天没成,还是用字符串了……
    老赵的版本足够解决这帖开头提到的问题了,是我把简单问题复杂化了 T T

  31. 老赵
    admin
    链接

    老赵 2009-09-07 19:00:00

    @RednaxelaFX
    是啊,() => p.Submit其实是为了得到事件的名字,可惜……强类型的我也不知道该怎么搞,只能字符串了。
    你了解过现在微软正在搞的Reactive Framework吗?其实这个框架也是使用字符串的,所以可能真没办法。

  32. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-07 19:03:00

    @Jeffrey Zhao
    我连换了好几种lambda的写法都不行,才无奈回到字符串的办法。所以刚才一看到() => p.Submit我就想起之前遇到的编译错误,就……
    Rx我还没看细节,到现在仅仅了解到它跟Observer模式的关系,以及跟IEnumerable的关系。Erik Meijer真神奇。不过原来它也是用字符串的么 =v=

  33. 老赵
    admin
    链接

    老赵 2009-09-07 19:06:00

    @RednaxelaFX
    是啊,它也是用字符串的。Erik Meijer是无比强大的人,各种编程模型一阵乱搞,搞得眼花缭乱。
    // 跑步去了。

  34. Anders Liu
    *.*.*.*
    链接

    Anders Liu 2009-09-07 19:08:00

    你的“‘统一添加’的辅助函数”为什么不把整个对象而不是事件传进去?

    老赵,说实话挺佩服你的技术,但感觉你lamda表达式用得有点过了……

  35. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-09-07 19:09:00

    貌似事件是 Immutable 的,你传进了 RegisterHandlers 后进行更改就产生了新的,这个跟传个 String 进去然后 += 的结果是一样的。

  36. 老赵
    admin
    链接

    老赵 2009-09-07 19:10:00

    @Anders Liu
    因为可能要为不同的事件添加处理器啊,比如需要这样复用:
    RegisterHandlers(new Program().Submit);
    RegisterHandlers(new MyClass().Submit);
    RegisterHandlers(new Another().Submit);

    我觉得……还好吧,一些不动点,的确是在玩。
    但是像现在这样的,目标还是良好的API设计,并非是为了Lambda而Lambda……

  37. 老赵
    admin
    链接

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

    @DiryBoy
    你说的似乎没错。

  38. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-09-07 19:34:00

    我的比较轻量级的解决办法,不过有点啰嗦

    class Program
    {
        event EventHandler Submit;
    
        static void RegisterHandlers( Action<EventHandler> adder )
        {
            adder(
                ( s, e ) =>
                {
                    Console.WriteLine("Good!");
                }
            );
        }
    
        static void Main( string[] args )
        {
            var p = new Program();
            RegisterHandlers(( e ) => p.Submit += e);
            p.Submit(p, EventArgs.Empty);
            Console.ReadKey();
        }
    }
    

  39. iceboundrock
    *.*.*.*
    链接

    iceboundrock 2009-09-07 20:07:00

    The event 'ConsoleApplication1.TestProgram.Submit' can only appear on the left hand side of += or -= (except when used from within the type 'ConsoleApplication1.TestProgram')

  40. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-07 20:07:00

    如果你不在Program类里,可能p.Submit这个表达式是非法的吧。。我记得x.EventName += XXX是个语句,而不是表达式。

  41. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-07 20:12:00

    在VB里,var.EventName这种语法在任何上下文都不是合法的表达式,它只能是语句AddHandler, RemoveHandler或者RaiseEvent的成分。在表达式树没有这三个语句之前,VB无法采用这种做法。

  42. 老赵
    admin
    链接

    老赵 2009-09-07 20:27:00

    装配脑袋:如果你不在Program类里,可能p.Submit这个表达式是非法的吧。。我记得x.EventName += XXX是个语句,而不是表达式。


    果然瞒不住脑袋,这就是我文章最后说的“糊弄”……

  43. 老赵
    admin
    链接

    老赵 2009-09-07 20:28:00

    @DiryBoy
    其实ref进去就可以了。

  44. 路过者[未注册用户]
    *.*.*.*
    链接

    路过者[未注册用户] 2009-09-07 20:53:00

    没看出老赵的目的到底要干什么

    有句老话

    Just Because You Can Do Something, Doesn't Mean You Should

  45. 星~无敌
    *.*.*.*
    链接

    星~无敌 2009-09-07 20:57:00

    一直关注

  46. 老赵
    admin
    链接

    老赵 2009-09-07 20:57:00

    @路过者
    放心,这些都是我在实际生产过程中遇到的问题,绝对有实用价值。
    “把事件当作对象进行传递”,就可以把这个事件到处传递到各处去处理。
    例如,丢给一个职责链……
    至于其他原因……尽请期待接下去的文章吧,呵呵。

  47. 阿亮002[未注册用户]
    *.*.*.*
    链接

    阿亮002[未注册用户] 2009-09-07 21:08:00

    我的目标就是争取看懂一篇老赵的文章。。。。

  48. toEverybody
    *.*.*.*
    链接

    toEverybody 2009-09-07 22:21:00

    我发现微软只是在搞语法糖,界面之类的,,,,
    应证了这句:微软是技术应用型公司,不是创新型公司...
    糖多了,就象肉多了一样会让人糊涂
    没有意思....开发出来的程序性能还是那样的差

  49. 老赵
    admin
    链接

    老赵 2009-09-07 22:36:00

    @toEverybody
    1、受限的只是你的眼光而已。你可以去微软研究院的网站看看世界顶级公司是怎么关注前沿科技的。微软令人佩服的就是既创新,又能把长尾收拾得服服贴贴。
    2、性能哪里差了?如果要FUD,建议学一下FSF和RMS是怎么搞专业化FUD的:http://www.fsf.org/news/dont-depend-on-mono,做什么都要专业。
    3、吃肉吃糊涂的人,问题往往不在提供肉的一方。责任一般在看到肉就乱吃,吃到反胃后又没良心地指责提供方的人,多发生于思维停留在幼年,还不懂事的时候。

  50. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-08 00:19:00

    装配脑袋:如果你不在Program类里,可能p.Submit这个表达式是非法的吧。。我记得x.EventName += XXX是个语句,而不是表达式。


    using System;
    
    static class Program {
        static event Action E;
        static void Main(string[] args) {
            Action a = () => {};
            var b = E += a;
            Console.WriteLine(b); // System.Action
        }
    }
    

    在定义事件的类内部,引用事件的名字其实引用到的是委托,所以+=不是语句来,只是普通的表达式,那个“+”是Delegate.Combine的糖。而在定义事件的类外部,引用事件的名字就变成是add_XXX跟remove_XXX方法的糖了,这两个方法返回void,虽然也是表达式但却无法跟其它表达式组合,要说是“语句”虽然不准确不过就是那个意思没错~

  51. 33333aa[未注册用户]
    *.*.*.*
    链接

    33333aa[未注册用户] 2009-09-08 07:40:00

    脱裤子放屁,图罗嗦

  52. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 08:15:00

    @RednaxelaFX

    即使是语法糖,编译时也有编译时的语义,这和编译后是什么样子无关。从Spec来看,确实没有专门的statement用于挂event。那么就只能是expression statement了,查看可以做expression statement的表达式中,只有assignment expression最符合这里的语义。

    SSCLI C#是个手写的递归下降解析器,有兴趣可以调试之看看是否是这样。

  53. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 08:27:00

    由此也可以推断,x.Event必须出现在等号左边以及该“赋值”返回void的特性是在语义分析阶段才进行的。只要有一个语法错误,C#就不会理会这些错误~

    从Build菜单完全编译下面的程序可以验证这一点:

    class Program
    {
        static void Main(string[] args)
        {
            A a = null;
    
            //语义错误要等语法正确才出现
            var x = a.E += new EventHandler(a_E);
    
            int //编译错误出现在这里,因为这是语法错误
        }
    
    
        static void a_E(object sender, EventArgs e)
        {
            throw new NotImplementedException();
        }
    }
    
    class A
    {
        public event EventHandler E;
    
    }
    

  54. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-08 08:42:00

    @装配脑袋
    正好以前因为别的一些问题我看过SSCLI里的C#编译器实现……不过那不重要。解糖本身就是编译时语义的一部分,只要是语法糖,都在编译器的控制范围内。

    按什么顺序解糖就会影响到能做到的事情的多少。解糖本来就是语义分析的一部分。如果lambda到ETv1的解糖先于运算符重载的解糖,那么在lambda body里出现了赋值运算符或复合赋值运算符都无法满足ETv1的限制,所以无法转换;如果顺序反过来,运算符重载的解糖先于lambda的,那么+=解糖为add_XXX的调用后,再转换为ETv1就不是问题了。

    微软版的C# spec一般是他们实现出了怎样的语言就怎么改spec,难免在spec里看到一些“遗留物”。当然也有些时候编辑spec太混乱导致spec本身就有问题(之前老赵发类型推导帖的时候我正好揪出了spec里的一个bug……虽然Eric说以前也有人发现过,他们会在C# 4的spec里修正)。

    再提点旧事……去年VS2010刚出CTP的时候,我跑到反馈论坛那边去提了个feature request,请求在C# 4里实现statement lambda到ETv2的自动转换。后面还有不少人跟帖支持的,可惜这个feature被拒了。后来我问Eric为什么拒,他说不是他们不想做,是时间经费等资源都有限,赶不上C# 4这趟了。
    微软的C#编译器是用C++写的,正如SSCLI里所见。它在做完语法分析之后直接就生成了比较低级的线性的中间表示。后面的可到达性分析、确定性赋值分析之类都是基于这种中间表示来做的。而这样的中间表示不适合再转换回到ETv2的形式(显然在当前实现里lambda的解糖在生成这种中间表示之前就完成了)。Eric把这个叫“premature lowering”。5月份我问的时候Eric说他们已经做了些重构,去除了大部分“premature lowering”,但还是来不及在C# 4推出前实现statement lambda到ETv2的转换。
    C# 4的spec里就不会看到允许statement lambda到ETv2的转换,因为微软自己还没来得及实现出来。就这么简单。

    当然并不是说编译器能够做的转换就都应该做,还得考虑到要保持语法表面的一致性。如果允许先把事件的+=解糖为add_XXX在做lambda的转换,+=的表面意义就在不同上下文中不一致了;而且还等于暴露了糖的细节,未必是好事。我抱怨C#编译器不肯做转换也是发发牢骚罢了……|||

  55. 李永京
    *.*.*.*
    链接

    李永京 2009-09-08 08:53:00

    @Jeffrey Zhao
    看看CSLA.NET框架就是在对象上操作,例如Save,就是Object.Save(),事件就不用说了。

  56. 老赵
    admin
    链接

    老赵 2009-09-08 09:03:00

    @李永京
    你不说,所以我还是没懂,hoho。
    Object.Save,说明它用的是类似于ActiveRecord的方式。
    或者说,持久化职责放在对象上了,这个……至少我不喜欢。

  57. 李永京
    *.*.*.*
    链接

    李永京 2009-09-08 09:07:00

    是啊,那框架就是一切的事情在对象上搞定,什么操作点点点点什么方法。这样性能也不好。原来看过一下的。

  58. 老赵
    admin
    链接

    老赵 2009-09-08 09:14:00

    33333aa:脱裤子放屁,图罗嗦


    有一句话叫做:每个漂亮的接口后面都有一个丑陋的实现。
    我的工作就是,把丑陋的东西集中起来掩盖掉,把美好的东西留在外面。
    现在展现的是丑陋的内部实现,如果您受不了的话,可以光看它的表面,呵呵。

  59. 老赵
    admin
    链接

    老赵 2009-09-08 09:15:00

    @李永京
    ActiveRecord主要用于快速把数据表映射成对象,其实连ORM都谈不上了……而且逻辑集中了,尤其是持久化逻辑。
    性能倒是另一回事了。

  60. 老赵
    admin
    链接

    老赵 2009-09-08 09:17:00

    @RednaxelaFX
    希望早日有Lambda statement到ETv2,这样可以释放程序员的创造力。

  61. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-08 09:19:00

    @Jeffrey Zhao
    嗯,早日大概也就是2012吧……Miguel在PDC2008上不是还开玩笑了么,“你们不用等到2012就可以用C# REPL了,通过Mono”

  62. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 09:24:00

    @RednaxelaFX

    首先我确定语义分析之前是不会理会语法糖的,在生成代码之前也不会去“解糖”。也就是说,你所谓的“线性中间表示”那个AST是不会包含诸如lambda的生成函数或者yield的生成emumerator类之类的语法树的。因此,小到event的assignment是不会在语法分析“解糖”的。因此无论如何,它是一个assignment expression而不是一个method call expression。

    至于语句ET,抱怨是没用的,自己动手吧,呵呵。ETv2作为AST,是完全不够C#和VB这种语义丰富的语言用的,不可能每个statement的节点都有,所以编译器实现时用的AST不可能直接是ETv2。不过做个转换很容易,特别是自己做着玩,不用太关心质量的时候。敬请期待NotBasic吧,我考虑增加Lisp一样的纯粹Expression Tree字面量,支持包括Lambda在内的任何表达式。还考虑用它来进行元编程支持。

  63. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-08 09:37:00

    @装配脑袋

    装配脑袋:
    @RednaxelaFX

    首先我确定语义分析之前是不会理会语法糖的,在生成代码之前也不会去“解糖”。也就是说,你所谓的“线性中间表示”那个AST是不会包含诸如lambda的生成函数或者yield的生成emumerator类之类的语法树的。因此,小到event的assignment是不会在语法分析“解糖”的。因此无论如何,它是一个assignment expression而不是一个method call expression。

    至于语句ET,抱怨是没用的,自己动手吧,呵呵。ETv2作为AST,是完全不够C#和VB这种语义丰富的语言用的,不可能每个statement的节点都有,所以编译器实现时用的AST不可能直接是ETv2。不过做个转换很容易,特别是自己做着玩,不用太关心质量的时候。敬请期待NotBasic吧,我考虑增加Lisp一样的纯粹Expression Tree字面量,支持包括Lambda在内的任何表达式。还考虑用它来进行元编程支持。


    generator的解糖就是在codegen之前做的……是所有糖中最后一种解的糖。在解generator糖之前,先要完成所有匿名方法的改写,其中包括逃逸分析,生成合适的闭包类型,生成合适的<>__XXX方法定义及其所属类型,等等。这些都是语义分析的一部分。
    我前面的回帖在“小到event的assignment是不会在语法分析“解糖”的”这点上也是持相同观点的嘛。但C#编译器内部结构中某个语法结构被表示成什么,跟后来生成的ET中那个结果被表示为什么,没有必然的直接映射;不是说C#编译器用的AST里某个东西是assignment它在生成的ET里也要是assignment。不然我要是写:
    var i = 1;
    Expression<Func<int>> e = () => i + 1;
    

    明明lambda里的i在C#的“语法表面”就是个SimpleName,要映射也应该是ParameterExpression,为什么实际映射出来的是MemberExpression呢?语法分析只能告诉我们i是个变量而已哦?因为涉及了改写,改写生成了闭包,i变成了闭包的成员域,而这些都是语义分析的时候做的。以前是这样,以后也是这样。

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

    小城故事 2009-09-08 10:00:00

    这个玩艺儿没事时可以玩下,如果确实有意义的话微软肯定会在未来版本framework中添加支持的,

  65. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 10:01:00

    @RednaxelaFX
    首先,Expression Tree的初始化语法语义分析都是和C#语法一起分析的,它并没有自己单独的步骤,所以这个例子说明不了什么。Expression Tree里并不会包含那个闭包类的Tree,目前的ET没有办法表达类(而是对i直接生成了ConstantExpression)。

    在语义分析的时候必须能够精确给出语法错误信息,所以要求尽量保全语法树是非常重要的。诸如using, lock, checked, unsafe,fixed的翻译,都应该是带着原始语法树进入语义分析的。

  66. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-08 10:08:00

    @装配脑袋

    using System;
    using System.Linq.Expressions;
    
    static class Program {
      static void Main(string[] args) {
        var i = 0;
        Expression<Func<int>> e = () => i + 1;
      }
    }
    

    Main的等价形式是:
    var display = new <>c__DisplayClass0 { i = 0 };
    Expression<Func<int>> expression = Expression.Lambda<Func<int>>(
      Expression.Add(
        Expression.Field(
          Expression.Constant(display),
          fieldof(<>c__DisplayClass0.i)
        ),
        Expression.Constant(1, typeof(int))
      ),
      new ParameterExpression[0]
    );
    

    i不是ConstantExpression,而是MemberExpression。
    开工了……回头再继续讨论

  67. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 10:08:00

    我实现过语义分析的代码。虽然我的逻辑比较粗糙,但是总体来说仍需要多个步骤来完成。首先resolve所有类型定义,然后是member定义,然后是member body中的type,然后是member本身,然后是局部变量。
    全都做完以前,是没有足够信息分析变量的逃逸的。

  68. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-08 10:15:00

    @装配脑袋

    装配脑袋:
    我实现过语义分析的代码。虽然我的逻辑比较粗糙,但是总体来说仍需要多个步骤来完成。首先resolve所有类型定义,然后是member定义,然后是member body中的type,然后是member本身,然后是局部变量。
    全都做完以前,是没有足够信息分析变量的逃逸的。


    我也不是没做过……诶。我猜在回帖里这么空打空的你也说服不了我,我也不想说服你,回头另外发帖讨论好了,大家都多准备点弹药来玩玩 ^ ^

  69. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 10:29:00

    @RednaxelaFX
    即使是那样,也没有逃出我所料啊。那个Expression Tree仍然是没法表示闭包的,它(C#编译器)静态生成了一个类来表示闭包的。

    不过Event+=的assignment expression在语义分析结束之前是不是保持原样,还是很容易试验的嘛。有空重新编译一下SSCLI调试它一下。

  70. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 10:31:00

    我认为讨论语法就不要想它是不是语法糖,没准CLR后序版本能够直接支持coroutine了,没准yield return就不那样实现了,等等。

    不考虑语法糖,语法分析和语义分析仍然可以严格进行,这样才好考虑语言的设计问题。

  71. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-08 10:37:00

    @装配脑袋

    装配脑袋:
    @RednaxelaFX
    即使是那样,也没有逃出我所料啊。那个Expression Tree仍然是没法表示闭包的,它(C#编译器)静态生成了一个类来表示闭包的。

    不过Event+=的assignment expression在语义分析结束之前是不是保持原样,还是很容易试验的嘛。有空重新编译一下SSCLI调试它一下。


    我想我从来没有表达过类型是“由ET生成的”的观点……ET确实表达不了闭包的类型,但它表现了闭包的代码实现,那就是通过Expression.Field生成MemberExpression。

    RednaxelaFX:
    generator的解糖就是在codegen之前做的……是所有糖中最后一种解的糖。在解generator糖之前,先要完成所有匿名方法的改写,其中包括逃逸分析,生成合适的闭包类型,生成合适的<>__XXX方法定义及其所属类型,等等。这些都是语义分析的一部分。


    这里的都是在C#编译器实际的内部实现的上下文里的讨论,还没到ET相关。

    装配脑袋:
    @RednaxelaFX
    首先,Expression Tree的初始化语法语义分析都是和C#语法一起分析的,它并没有自己单独的步骤,所以这个例子说明不了什么。Expression Tree里并不会包含那个闭包类的Tree,目前的ET没有办法表达类(而是对i直接生成了ConstantExpression)。


    嗯我们在什么地方把对方句子看偏差了么…… =v=||||| anyway

  72. AlexLiu
    *.*.*.*
    链接

    AlexLiu 2009-09-08 10:44:00

    如果能把每次赵老师的文章彻底看懂。我想我会很满足。

  73. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 10:48:00

    @RednaxelaFX
    你害我早上没有写完我想写的Blog……555
    其实我本来就觉得没有什么不清楚的地方=_=b sorry

  74. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-08 10:51:00

    @装配脑袋

    装配脑袋:
    @RednaxelaFX
    你害我早上没有写完我想写的Blog……555
    其实我本来就觉得没有什么不清楚的地方=_=b sorry


    我本来也没觉得有什么不清楚的地方 =__,=
    诶我上午要做的事情看来也完不成了 T.T
    杯具啊

  75. 道法自然
    *.*.*.*
    链接

    道法自然 2009-09-08 10:52:00

    class MyTestEvents
    {
    private IList<EventHandler> _testEventHandlers = new List<EventHandler>();

    public event EventHandler TestEvent
    {
    add
    {
    _testEventHandlers.Add(value);
    }
    remove
    {
    _testEventHandlers.Remove(value);
    }
    }
    }
    这是原始事件的定义,传递_testEventHandlers不就可以了吗?

  76. AlexLiu
    *.*.*.*
    链接

    AlexLiu 2009-09-08 10:57:00

    Event里面的+=好像就是Delegate.Combine(……);

  77. 老赵
    admin
    链接

    老赵 2009-09-08 10:59:00

    @道法自然
    哪儿有,原始定义是基于MulticastDelegate的:

    [MethodImpl(MethodImplOptions.Synchronized)]
    public void add_Submit(EventHandler value)
    {
        this.Submit = (EventHandler) Delegate.Combine(this.Submit, value);
    }
    

  78. 老赵
    admin
    链接

    老赵 2009-09-08 11:00:00

    @RednaxelaFX
    你们两个都很牛,每次都让我看的很爽。
    看到你们就看到差距了,唉……

  79. 道法自然
    *.*.*.*
    链接

    道法自然 2009-09-08 11:20:00

    @Jeffrey Zhao

    class MyEventHandlers
    {
    	private Delegate _testEventHandler;
    	
    	public event EventHandler TestEvent
    	{
    		add
    		{
    			_testEventHandler = Delegate.Combine(_testEventHandler, value);
    		}
    		remove
    		{
    			Delegate.Remove(_testEventHandler, value);
    		}
    	}
    	
    	public void FireEvent()
    	{
    		object sender = new object();
    		EventArgs e = new EventArgs();
    		
    		if(_testEventHandler != null)
    		{
    			_testEventHandler.DynamicInvoke(sender, e);
    		}
    	}
    }
    


    我的原始定义,是指事件的定义方式,可以通过这样的方式来实现。

  80. 老赵
    admin
    链接

    老赵 2009-09-08 11:22:00

    @道法自然
    如果手动这么做,自然没有问题,呵呵。

  81. 道法自然
    *.*.*.*
    链接

    道法自然 2009-09-08 11:23:00

    不过,我现在没有跟上DOTNET 2和3了,需要赶上。

  82. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-08 13:38:00

    装配脑袋:在VB里,var.EventName这种语法在任何上下文都不是合法的表达式,它只能是语句AddHandler, RemoveHandler或者RaiseEvent的成分。在表达式树没有这三个语句之前,VB无法采用这种做法。



    实验了一下只要不是在类内的代码访问定义于类的事件,都不允许这么做。

    为什么说是类内的代码?
    是因为内嵌类可以那啥。。。。真晕。。。。

    当然,如果一旦事件写了add或者remove,也就是语法错误了。。。

  83. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-08 13:43:00

    VB应该也可以吧,否则if( eventName == null )怎么写。刚想到这个问题,顺便再做了一些测试,在类型的内部,竟然event = null都是可以编译的,但如果用了add和remove,则event == null都是非法的。。。。

  84. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 14:22:00

    Ivony...:VB应该也可以吧,否则if( eventName == null )怎么写。刚想到这个问题,顺便再做了一些测试,在类型的内部,竟然event = null都是可以编译的,但如果用了add和remove,则event == null都是非法的。。。。



    看来真是不了解VB啊,呵呵…… :P

  85. 高手中的高手[未注册用户]
    *.*.*.*
    链接

    高手中的高手[未注册用户] 2009-09-08 17:49:00

    你看你就是个菜鸟。 这么简单也不懂。

  86. 高手中的高手[未注册用户]
    *.*.*.*
    链接

    高手中的高手[未注册用户] 2009-09-08 17:50:00

    还要那相片,照得又难看。摆的动作又土,还Y先,不多说了。说多了伤自尊。

  87. 老赵
    admin
    链接

    老赵 2009-09-08 18:01:00

    @高手中的高手
    高手多说点,指导一下我这菜鸟嘛。

  88. overred
    *.*.*.*
    链接

    overred 2009-09-09 13:26:00

    static void RegisterHandlers(ref EventHandler ev)...

  89. Kevin Dai
    *.*.*.*
    链接

    Kevin Dai 2009-12-13 15:42:00

    Rx出来了,这下子爽了,哈哈

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我