在lambda表达式中的事件 - C#编译器bug?

10

我想使用lambda表达式以强类型方式连接事件,并在中间加入监听器,例如给定以下类

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: 表达式树不能包含赋值运算符

一开始看起来似乎很合理,特别是在阅读了有关为什么表达式树不能包含赋值语句的解释之后。然而,尽管使用了 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)
所以在我看来,这似乎是编译器的一个错误,因为它抱怨不允许赋值,但实际上并没有赋值,只是一个方法调用。或者我有什么遗漏吗...?
编辑:请注意,问题是“这种行为是编译器的bug吗?”如果我没有清楚地表达我的问题,对不起。
编辑2:阅读Inferis的答案后,他说“在那个点上,+= 被认为是赋值”,这有些合理,因为此时编译器可以认为它将转化为CIL。但是我不被允许使用显式的方法调用形式:
Listener.WireUp<Producer, Consumer>(
    (p, c) => p.add_MyEvent(new EventHandler(c.MyHandler)));

给出:

CS0571: 'Producer.MyEvent.add':不能显式调用运算符或访问器

所以,我想问题归结为在C#事件的上下文中,+=实际上意味着什么。它是指“调用此事件的add方法”还是指“以尚未定义的方式添加到此事件”。如果是前者,则对我来说这似乎是编译器错误,而如果是后者,则可能不太直观,但可以说不是错误。思路?


1
+= 的意思是“调用给定事件的事件访问器”。我认为这不算是一个 bug,而是一个稍微烦人且可以说是不必要的限制。 - Jon Skeet
5个回答

5
在规范中,第7.16.3节将+=和-=运算符称为“事件赋值”,这确实使它听起来像是一个赋值运算符。它在第7.16节(“赋值运算符”)中的事实是一个相当大的提示 :) 从这个角度来看,编译器错误是有道理的。
然而,我同意它过于严格了,因为表达式树完全可以表示由lambda表达式给出的功能。
我怀疑语言设计者采用了“稍微更严格但在运算符描述上更一致”的方法,以牺牲像这样的情况。

Jon - 是的,我刚才就是在那里(请参见我的编辑#2)。因此,我认为规范所指示的是+=与事件意味着“以尚未定义的方式分配给事件”,而不是“为我调用添加方法”。如果是这样的话,这将令人沮丧,但从技术上讲并不是一个错误。 - Greg Beech
我认为它甚至不是“尚未定义” - 它可以识别该方法并发出调用该方法的表达式树。这将是合理的行为,除了它会处理一个仍然严格意义上是赋值运算符的运算符。 - Jon Skeet
好的,你说服我了;我认为这在技术上是一个赋值运算符,这意味着这不是一个错误。但我们都知道,在底层它实际上是一个方法调用,这让人感到沮丧...唉,好吧 :-S - Greg Beech
你能否尝试使用System.MulticastDelegate.Combine来添加事件处理程序呢?顺便说一下,我仍然认为在我的编辑帖子中给出的解决方案应该可以完成工作。 - Noldorin
@Noldorin - 不幸的是,似乎任何尝试做任何聪明的事情(包括调用Combine)都会导致错误CS0070:事件'Producer.MyEvent'只能出现在+=或-=的左手边(除非从类型'Producer'内部使用)。 - Greg Beech
@Noldorin:不,你不能使用Delegate.Combine——这是在add_EventHandler部分中完成的,在事件(通常)中完成。事件只是添加/删除方法。 - Jon Skeet

1

+= 是一种赋值运算符,无论它执行什么操作(例如添加事件),从解析器的角度来看,它仍然是一种赋值。

你试过了吗?

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

解析器可能会将其视为赋值,但正如我所说,从语义和物理上来看,它不是赋值,而是方法调用。这就是我所说的似乎是编译器错误的意思。编译器应该知道在这种情况下,它不是赋值。 - Greg Beech
此外,将花括号放在表达式周围意味着它不再是lambda,而是一个普通的委托,这意味着它无法转换为表达式树。 - Greg Beech
我一开始并没有理解整个监听器结构的意义 :/ - Timbo
WireUp方法是如何实现的? - Timbo
@Greg:我大部分同意你的观点,但不同意你的说法:“在表达式周围放置花括号意味着它不再是lambda表达式”。它仍然是一个lambda表达式,只是带有块体。 - Jon Skeet
显示剩余3条评论

1
为什么要使用Expression类?在您的代码中将Expression<Action<TProducer, TConsumer>>更改为简单的Action<TProducer, TConsumer>,所有内容都应该按照您想要的方式工作。您在这里所做的是强制编译器将lambda表达式视为表达式树而不是委托,而表达式树确实不能包含此类赋值(我认为它被视为赋值,因为您正在使用+=运算符)。现在,lambda表达式可以转换为任一形式(如[MSDN][1]所述)。通过仅使用委托(Action类就是这样),这样的“赋值”是完全有效的。我可能误解了问题(也许您需要使用表达式树的特定原因?),但幸运的是,解决方案似乎很简单!
编辑:好的,我从评论中更好地理解了您的问题。您是否有任何理由不能将p.MyEvent和c.MyHandler作为参数传递给WireUp方法,并在WireUp方法内附加事件处理程序(对我来说,这也从设计角度看起来更好)...那不会消除表达式树的需要吗?我认为最好避免使用表达式树,因为它们往往比委托慢得多。

因为我需要访问表达式树才能抓取应该连接哪个事件和哪个处理程序方法。如果没有表达式树,你就做不到这一点。 - Greg Beech
好的,我的帖子现在已经更新,提供了另一种解决方案。 - Noldorin
1
很遗憾,似乎没有强类型的方法来获取p.MyEvent - 尝试在没有使用+=的情况下使用它会导致错误CS0070:事件'Producer.MyEvent'只能出现在+=或-=的左侧(除非从类型'Producer'内部使用)。 - Greg Beech

1

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

所以编译器所做的是:创建一个表达式,在其中将 c.MyHandler 添加到 p.MyEvent 的当前值中,并将更改后的值存回到 p.MyEvent 中。因此,即使最终你并没有进行赋值,实际上你仍然在进行赋值。

你想让 WireUp 方法接受一个表达式而不是只接受一个 Action,有什么原因吗?


因为我需要访问表达式树才能抓取应该连接哪个事件和哪个处理程序方法。如果没有表达式树,你就做不到这一点。 - Greg Beech
但是,我明白你的意思,我认为这个答案的关键部分是“在那时候”...让我编辑一下问题。 - Greg Beech

0

我认为问题在于,除了Expression<TDelegate>对象之外,从编译器的角度来看,表达式树没有静态类型。 MethodCallExpression和其他相关类不公开静态类型信息。

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

尽管如此,我仍然认为应该向Microsoft提交此问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接