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

时间:2009-02-19 11:55:27

标签: c# compiler-construction lambda compiler-errors

我正在考虑使用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中看到的那样,如果我们只是将事件连接起来通常:

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':无法显式调用operator或accessor

所以,我想这个问题归结为+=在C#事件的上下文中实际意味着什么。这是否意味着“为此事件调用add方法”或者是否意味着“以尚未定义的方式添加到此事件”。如果它是前者,那么在我看来这是一个编译器错误,而如果它是后者那么它有点不直观但可以说不是一个错误。想法?

5 个答案:

答案 0 :(得分:5)

在规范7.16.3节中,+ =和 - =运算符被称为“事件赋值”,它肯定会使它听起来像赋值运算符。事实上,它在7.16节(“赋值运算符”)中是一个相当大的提示:)从这个角度来看,编译器错误是有道理的。

但是,我同意 过度限制,因为表达式树完全有可能表示lambda表达式给出的功能。

怀疑语言设计师采用“稍微限制但操作员描述更加一致”的方法,以牺牲这样的情况为代价,我担心。

答案 1 :(得分:1)

+ =是一项任务,无论它做什么(例如添加一个事件)。从解析器的角度来看,它仍然是一项任务。

你试过吗

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

答案 2 :(得分:1)

为什么要使用Expression类?将代码中的Expression<Action<TProducer, TConsumer>>更改为Action<TProducer, TConsumer>,所有内容都可以按您的意愿运行。你在这里做的是迫使编译器将lambda表达式视为表达式树而不是委托,并且表达式树确实不能包含这样的赋值(它被视为赋值,因为你使用的是+ =运算符我相信)。现在,lambda表达式可以转换为任何一种形式(如[MSDN] [1]中所述)。通过简单地使用委托(这是所有的Action类),这样的“赋值”是完全有效的。我可能在这里误解了这个问题(可能有一个特定的原因,你需要使用表达式树?),但似乎解决方案似乎很幸运,这很简单!

编辑:是的,我现在从评论中了解您的问题。有没有什么理由你不能只将p.MyEvent和c.MyHandler作为参数传递给WireUp方法并在WireUp方法中附加事件处理程序(对我而言,从设计的角度看这似乎也更好)...会那不能消除对表达式树的需求吗?我认为最好还是避免使用表达树,因为与代表相比,它们往往相当慢。

答案 3 :(得分:1)

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

编译器的作用是:创建一个表达式,将c.MyHandler添加到p.MyEvent的当前值,并将更改后的值存储回p.MyEvent。所以你实际上正在做一项任务,即使最终你不是。

您是否有理由希望WireUp方法采用表达式而不仅仅是Action?

答案 4 :(得分:0)

我认为问题是,除了Expression<TDelegate>对象之外,表达式树不是从编译器的角度静态类型化的。 MethodCallExpression和朋友不公开静态输入信息。

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

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