【问题标题】:Events in lambda expressions - C# compiler bug?lambda 表达式中的事件 - C# 编译器错误?
【发布时间】:2009-02-19 11:55:26
【问题描述】:

我正在考虑使用 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”:无法显式调用运算符或访问器

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

【问题讨论】:

  • += 表示“调用给定事件的事件访问器”。我认为这不是一个错误,而是一个有点烦人且可以说是不必要的限制。

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


【解决方案1】:

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

但是,我同意它过于严格,因为表达式树完全有可能代表 lambda 表达式所提供的功能。

怀疑语言设计者采用了“限制性稍强但运算符描述更一致”的方法,恐怕会以牺牲这种情况为代价。

【讨论】:

  • 乔恩 - 是的,这就是我刚刚到达的地方(见我的编辑#2)。所以我认为规范表明的是 += with events 意味着“以尚未定义的方式分配给事件”而不是“为我调用 add 方法”。在这种情况下,它会令人沮丧,但从技术上讲不是错误。
  • 我认为它甚至不是“尚未定义”——它可以识别方法并发出一个调用它的表达式树。这将是合理的行为,除了它将处理一个仍然 - 严格来说 - 一个赋值运算符的运算符。
  • 好吧,你说服了我;我认为它在技术上是一个赋值运算符,这意味着这不是一个错误。但这很令人沮丧,因为我们都知道这实际上是一个方法调用......嗯:-S
  • 难道您不能使用 System.MulticastDelegate.Combine 添加事件处理程序吗?顺便说一句,我仍然认为我编辑的帖子中给出的解决方案应该可以完成这项工作。
  • @Noldorin - 不幸的是,任何聪明的尝试(包括调用组合)似乎都会导致错误 CS0070:事件“Producer.MyEvent”只能出现在 += 的左侧或-=(在“生产者”类型中使用时除外)
【解决方案2】:

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

你试过了吗

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

【讨论】:

  • 解析器可能将其视为赋值,但正如我所说,从语义和物理上讲,它不是赋值,而是方法调用。这就是我所说的它似乎是一个编译器错误的意思。编译器应该知道,在这种情况下,它不是赋值。
  • 另外,在表达式周围加上大括号意味着它不再是一个 lambda,它是一个普通的委托,这意味着它不能被转换为一个表达式树。
  • 一开始我没有明白整个监听器结构的意义:/
  • WireUp 方法是如何实现的?
  • @Greg:我在很大程度上同意,但不同意您的说法,即“在表达式周围加上花括号意味着它不再是 lambda”。它仍然是一个 lambda 表达式,但带有一个块体。
【解决方案3】:

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

所以编译器会说:创建一个表达式,在其中将c.MyHandler 添加到p.MyEvent 的当前值,并将更改后的值存储回p.MyEvent。所以你实际上是在做一个任务,即使最后你没有。

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

【讨论】:

  • 因为我需要访问表达式树才能获取应该连接哪个事件和哪个处理程序方法。如果没有表达式树,您将无法做到这一点。
  • 但是,我明白你的意思,我认为这个答案的关键部分是“在那一点”......让我编辑这个问题。
【解决方案4】:

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

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

【讨论】:

  • 因为我需要访问表达式树才能获取应该连接哪个事件和哪个处理程序方法。如果没有表达式树,您将无法做到这一点。
  • 很公平。我的帖子现已更新以提供替代解决方案。
  • 不幸的是,似乎没有任何强类型的方法来获取 p.MyEvent - 尝试在没有 += 的情况下使用它会导致错误 CS0070: The event 'Producer.MyEvent' can仅出现在 += 或 -= 的左侧(在“生产者”类型中使用时除外)
【解决方案5】:

我认为问题在于,除了Expression&lt;TDelegate&gt; 对象之外,从编译器的角度来看,表达式树不是静态类型的。 MethodCallExpression 和朋友不公开静态类型信息。

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

不过我还是会考虑将此提交给微软。

【讨论】:

    猜你喜欢
    • 2017-01-06
    • 1970-01-01
    • 2011-07-15
    • 2012-08-01
    • 1970-01-01
    • 2011-07-30
    • 1970-01-01
    • 2015-01-11
    • 1970-01-01
    相关资源
    最近更新 更多