10

我正在研究使用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:表达式树可能不包含赋值运算符

现在起初这似乎是合理的,尤其是在阅读了关于为什么表达式树不能包含赋值的解释之后。然而,尽管有 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 方法”还是“以尚未定义的方式添加到该事件”。如果是前者,那么这在我看来是一个编译器错误,而如果是后者,那么它有点不直观,但可以说不是一个错误。想法?

4

5 回答 5

5

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

但是,我同意它过于严格,因为表达式树完全有可能表示 lambda 表达式给出的功能。

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

于 2009-02-19T12:21:13.383 回答
1

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

你试过了吗

Listener.WireUp<Producer, Consumer>((p, c) => { p.MyEvent += c.MyHandler; } );
于 2009-02-19T12:03:11.930 回答
1

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

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

于 2009-02-19T12:10:13.317 回答
1

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

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

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

于 2009-02-19T12:11:26.560 回答
0

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

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

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

于 2009-02-19T12:11:11.163 回答