代码之家  ›  专栏  ›  技术社区  ›  Greg Beech

lambda表达式中的事件-C编译器错误?

  •  10
  • Greg Beech  · 技术社区  · 16 年前

    我正在考虑使用lamba表达式来允许事件以强类型方式连接起来,但是中间有一个监听器,例如给定以下类

    class Producer
    {
        public event EventHandler MyEvent;
    }
    
    class Consumer
    {
        public void MyHandler(object sender, EventArgs e) { /* ... */ }
    }
    
    class Listener
    {
        public static void WireUp<TProducer, TConsumer>(
            Expression<Action<TProducer, TConsumer>> expr) { /* ... */ }
    }
    

    一个事件将被连接为:

    Listener.WireUp<Producer, Consumer>((p, c) => p.MyEvent += c.MyHandler);
    

    但是,这会导致编译器错误:

    CS0832:表达式树不能包含赋值运算符

    一开始这似乎是合理的,尤其是在 reading the explanation about why expression trees cannot contain assignments . 然而,尽管使用了C语法, += 不是分配,而是调用 Producer::add_MyEvent 方法,正如我们从CIL中看到的,如果我们只是正常连接事件,则会生成CIL:

    L_0001: newobj instance void LambdaEvents.Producer::.ctor()
    L_0007: newobj instance void LambdaEvents.Consumer::.ctor()
    L_000f: ldftn instance void LambdaEvents.Consumer::MyHandler(object, class [mscorlib]System.EventArgs)
    L_0015: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
    L_001a: callvirt instance void LambdaEvents.Producer::add_MyEvent(class [mscorlib]System.EventHandler)
    

    所以在我看来这是一个编译器错误,因为它抱怨不允许分配,但是没有分配发生,只是一个方法调用。或者我错过了什么……?

    编辑:

    请注意,问题是“这种行为是编译器错误吗?”对不起,如果我不清楚我在问什么。

    编辑2

    在阅读了Inferis的答案后,他说“在那个时候,+=被认为是赋值”,这确实有一些道理,因为此时编译器可能不知道它会变成CIL。

    但是,不允许我编写显式方法调用窗体:

    Listener.WireUp<Producer, Consumer>(
        (p, c) => p.add_MyEvent(new EventHandler(c.MyHandler)));
    

    给予:

    cs0571:“producer.myEvent.add”:无法显式调用运算符或访问器

    所以,我想问题归根结底是什么 += 实际上是指在C事件的背景下。它是指“为此事件调用Add方法”还是指“以尚未定义的方式添加到此事件”。如果是前者,那么在我看来这是一个编译器bug,而如果是后者,那么它有点不具有逻辑性,但可以说不是bug。思想?

    5 回复  |  直到 16 年前
        1
  •  5
  •   Jon Skeet    16 年前

    在规范第7.16.3节中,+=和-=操作符被称为“事件分配”,这无疑使它听起来像一个分配操作符。事实上,它在第7.16节(“赋值运算符”)中是一个非常大的提示:)从这个角度来看,编译器错误是有意义的。

    但是,我同意 过度限制,因为表达式树完全可以表示lambda表达式所提供的功能。

    犯罪嫌疑人 语言设计人员采用了“稍微更严格但更一致的操作员描述”的方法,恐怕会以这种情况为代价。

        2
  •  1
  •   Timbo    16 年前

    +=是一个分配,不管它做什么(例如添加一个事件)。从解析器的角度来看,它仍然是一个赋值。

    你试过吗?

    Listener.WireUp<Producer, Consumer>((p, c) => { p.MyEvent += c.MyHandler; } );
    
        3
  •  1
  •   Inferis    16 年前

    实际上,就编译器而言, 作业。 +=运算符被重载,但编译器并不关心它的作用。毕竟,您正在通过lambda生成一个表达式(在某一点上,它将被编译为实际的代码),而不是实际的代码。

    所以编译器要做的是:在添加 c.MyHandler 到当前值 p.MyEvent 将更改后的值存储回 P.MyEng事件 . 所以你实际上在做一个作业,即使最终你没有。

    您是否希望wireup方法采用表达式而不仅仅是操作?

        4
  •  1
  •   Noldorin    16 年前

    为什么要使用表达式类?变化 Expression<Action<TProducer, TConsumer>> 在你的代码中 Action<TProducer, TConsumer> 一切都应该按你的意愿去做。您在这里所做的是强制编译器将lambda表达式视为表达式树而不是委托,并且表达式树确实不能包含此类赋值(它被视为赋值,因为您使用的是我相信的+=运算符)。现在,lambda表达式可以转换为任意一种形式(如[msdn][1]所述)。通过简单地使用委托(这就是所有的动作类),这样的“分配”是完全有效的。我可能误解了这里的问题(也许您需要使用表达式树的具体原因?)但看起来解决方案确实如此简单!

    编辑: 好吧,我现在从评论中更好地理解了你的问题。您是否有任何理由不能将p.myEvent和c.myHandler作为参数传递给wireup方法,并将事件处理程序附加到wireup方法中(对我来说,从设计的角度来看,这似乎更好)。这不会消除对表达式树的需求吗?我认为最好还是不要使用表情树,因为与代表相比,表情树的速度要慢得多。

        5
  •  0
  •   Christian Klauser    16 年前

    我认为问题是,除了 Expression<TDelegate> 对象,表达式树不是从编译器的角度静态类型化的。 MethodCallExpression 而且朋友不会暴露静态类型信息。

    即使编译器知道表达式中的所有类型,在将lambda表达式转换为表达式树时,也会丢弃这些信息。(查看为表达式树生成的代码)

    尽管如此,我还是会考虑提交给微软。