While investigating this question I got curious about how the new covariance/contravariance features in C# 4.0 will affect it.
在调查这个问题时,我很好奇C#4.0中新的协方差/逆变特性将如何影响它。
In Beta 1, C# seems to disagree with the CLR. Back in C# 3.0, if you had:
在Beta 1中,C#似乎不同意CLR。回到C#3.0,如果你有:
public event EventHandler<ClickEventArgs> Click;
... and then elsewhere you had:
......然后你在其他地方:
button.Click += new EventHandler<EventArgs>(button_Click);
... the compiler would barf because they're incompatible delegate types. But in C# 4.0, it compiles fine, because in CLR 4.0 the type parameter is now marked as in
, so it is contravariant, and so the compiler assumes the multicast delegate +=
will work.
...编译器会barf,因为它们是不兼容的委托类型。但是在C#4.0中,编译很好,因为在CLR 4.0中,类型参数现在标记为in,因此它是逆变的,因此编译器假定多播委托+ =将起作用。
Here's my test:
这是我的测试:
public class ClickEventArgs : EventArgs { }
public class Button
{
public event EventHandler<ClickEventArgs> Click;
public void MouseDown()
{
Click(this, new ClickEventArgs());
}
}
class Program
{
static void Main(string[] args)
{
Button button = new Button();
button.Click += new EventHandler<ClickEventArgs>(button_Click);
button.Click += new EventHandler<EventArgs>(button_Click);
button.MouseDown();
}
static void button_Click(object s, EventArgs e)
{
Console.WriteLine("Button was clicked");
}
}
But although it compiles, it doesn't work at runtime (ArgumentException
: Delegates must be of the same type).
但是虽然它编译,但它在运行时不起作用(ArgumentException:Delegates必须是相同的类型)。
It's okay if you only add either one of the two delegate types. But the combination of two different types in a multicast causes the exception when the second one is added.
如果你只添加两种委托类型中的任何一种,那也没关系。但是,当添加第二个时,多播中两种不同类型的组合会导致异常。
I guess this is a bug in the CLR in beta 1 (the compiler's behaviour looks hopefully right).
我想这是beta 1中CLR中的一个错误(编译器的行为看起来很有希望)。
Update for Release Candidate:
候选发布更新:
The above code no longer compiles. It must be that the contravariance of TEventArgs
in the EventHandler<TEventArgs>
delegate type has been rolled back, so now that delegate has the same definition as in .NET 3.5.
上面的代码不再编译。必须是回退了EventHandler
That is, the beta I looked at must have had:
也就是说,我看过的测试版必须具备:
public delegate void EventHandler<in TEventArgs>(object sender, TEventArgs e);
Now it's back to:
现在回到:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
But the Action<T>
delegate parameter T
is still contravariant:
但是Action
public delegate void Action<in T>(T obj);
The same goes for Func<T>
's T
being covariant.
Func
This compromise makes a lot of sense, as long as we assume that the primary use of multicast delegates is in the context of events. I've personally found that I never use multicast delegates except as events.
只要我们假设多播委托的主要用途是在事件的上下文中,这种妥协就很有意义。我个人发现除了作为事件之外,我从不使用多播代理。
So I guess C# coding standards can now adopt a new rule: don't form multicast delegates from multiple delegate types related through covariance/contravariance. And if you don't know what that means, just avoid using Action
for events to be on the safe side.
所以我猜C#编码标准现在可以采用一个新规则:不要通过协方差/逆变来形成与多个代理类型相关的多播代理。如果您不知道这意味着什么,请避免使用Action来保证事件安全。
Of course, that conclusion has implications for the original question that this one grew from...
当然,这个结论对于这个问题的原始问题有影响......
3 个解决方案
#1
Very interesting. You don't need to use events to see this happening, and indeed I find it simpler to use simple delegates.
很有意思。您不需要使用事件来查看这种情况,事实上我发现使用简单的委托更简单。
Consider Func<string>
and Func<object>
. In C# 4.0 you can implicitly convert a Func<string>
to Func<object>
because you can always use a string reference as an object reference. However, things go wrong when you try to combine them. Here's a short but complete program demonstrating the problem in two different ways:
考虑Func
using System;
class Program
{
static void Main(string[] args)
{
Func<string> stringFactory = () => "hello";
Func<object> objectFactory = () => new object();
Func<object> multi1 = stringFactory;
multi1 += objectFactory;
Func<object> multi2 = objectFactory;
multi2 += stringFactory;
}
}
This compiles fine, but both of the Combine
calls (hidden by the += syntactic sugar) throw exceptions. (Comment out the first one to see the second one.)
这编译很好,但两个Combine调用(由+ =语法糖隐藏)抛出异常。 (注释第一个看第二个。)
This is definitely a problem, although I'm not exactly sure what the solution should be. It's possible that at execution time the delegate code will need to work out the most appropriate type to use based on the delegate types involved. That's a bit nasty. It would be quite nice to have a generic Delegate.Combine
call, but you couldn't really express the relevant types in a meaningful way.
这绝对是一个问题,虽然我不确定解决方案应该是什么。在执行时,委托代码可能需要根据所涉及的委托类型计算出最合适的类型。那有点讨厌。有一个通用的Delegate.Combine调用会很好,但你无法真正以有意义的方式表达相关类型。
One thing that's worth noting is that the covariant conversion is a reference conversion - in the above, multi1
and stringFactory
refer to the same object: it's not the same as writing
值得注意的一件事是协变转换是一个引用转换 - 在上面,multi1和stringFactory引用相同的对象:它与写入不同
Func<object> multi1 = new Func<object>(stringFactory);
(At that point, the following line will execute with no exception.) At execution time, the BCL really does have to deal with a Func<string>
and a Func<object>
being combined; it has no other information to go on.
(此时,以下行将执行,没有异常。)在执行时,BCL确实必须处理正在组合的Func
It's nasty, and I seriously hope it gets fixed in some way. I'll alert Mads and Eric to this question so we can get some more informed commentary.
这很讨厌,我真的希望它能以某种方式得到修复。我会提醒Mads和Eric这个问题所以我们可以得到一些更明智的评论。
#2
I just had to fix this in my application. I did the following:
我只需要在我的应用程序中解决这个问题。我做了以下事情:
// variant delegate with variant event args
MyEventHandler<<in T>(object sender, IMyEventArgs<T> a)
// class implementing variant interface
class FiresEvents<T> : IFiresEvents<T>
{
// list instead of event
private readonly List<MyEventHandler<T>> happened = new List<MyEventHandler<T>>();
// custom event implementation
public event MyEventHandler<T> Happened
{
add
{
happened.Add(value);
}
remove
{
happened.Remove(value);
}
}
public void Foo()
{
happened.ForEach(x => x.Invoke(this, new MyEventArgs<T>(t));
}
}
I don't know if there are relevant differences to regular multi-cast events. As far as I used it, it works ...
我不知道常规多播事件是否存在相关差异。就我用它而言,它有效......
By the way: I never liked the events in C#. I don't understand why there is a language feature, when it doesn't provide any advantages.
顺便说一下:我从不喜欢C#中的事件。我不明白为什么有语言功能,当它没有提供任何优势。
#3
Are you getting the ArgumentException from both? If the exception is being thrown by just the new handler, then I would think that it's backward-compatible.
你从两者得到ArgumentException吗?如果仅通过新处理程序抛出异常,那么我认为它是向后兼容的。
BTW, I think you have your comments mixed up. In C# 3.0 this:
顺便说一句,我想你的意见混淆了。在C#3.0中:
button.Click += new EventHandler<EventArgs>(button_Click); // old
button.Click + = new EventHandler
wouldn't have run. That's c#4.0
不会跑。那是c#4.0
#1
Very interesting. You don't need to use events to see this happening, and indeed I find it simpler to use simple delegates.
很有意思。您不需要使用事件来查看这种情况,事实上我发现使用简单的委托更简单。
Consider Func<string>
and Func<object>
. In C# 4.0 you can implicitly convert a Func<string>
to Func<object>
because you can always use a string reference as an object reference. However, things go wrong when you try to combine them. Here's a short but complete program demonstrating the problem in two different ways:
考虑Func
using System;
class Program
{
static void Main(string[] args)
{
Func<string> stringFactory = () => "hello";
Func<object> objectFactory = () => new object();
Func<object> multi1 = stringFactory;
multi1 += objectFactory;
Func<object> multi2 = objectFactory;
multi2 += stringFactory;
}
}
This compiles fine, but both of the Combine
calls (hidden by the += syntactic sugar) throw exceptions. (Comment out the first one to see the second one.)
这编译很好,但两个Combine调用(由+ =语法糖隐藏)抛出异常。 (注释第一个看第二个。)
This is definitely a problem, although I'm not exactly sure what the solution should be. It's possible that at execution time the delegate code will need to work out the most appropriate type to use based on the delegate types involved. That's a bit nasty. It would be quite nice to have a generic Delegate.Combine
call, but you couldn't really express the relevant types in a meaningful way.
这绝对是一个问题,虽然我不确定解决方案应该是什么。在执行时,委托代码可能需要根据所涉及的委托类型计算出最合适的类型。那有点讨厌。有一个通用的Delegate.Combine调用会很好,但你无法真正以有意义的方式表达相关类型。
One thing that's worth noting is that the covariant conversion is a reference conversion - in the above, multi1
and stringFactory
refer to the same object: it's not the same as writing
值得注意的一件事是协变转换是一个引用转换 - 在上面,multi1和stringFactory引用相同的对象:它与写入不同
Func<object> multi1 = new Func<object>(stringFactory);
(At that point, the following line will execute with no exception.) At execution time, the BCL really does have to deal with a Func<string>
and a Func<object>
being combined; it has no other information to go on.
(此时,以下行将执行,没有异常。)在执行时,BCL确实必须处理正在组合的Func
It's nasty, and I seriously hope it gets fixed in some way. I'll alert Mads and Eric to this question so we can get some more informed commentary.
这很讨厌,我真的希望它能以某种方式得到修复。我会提醒Mads和Eric这个问题所以我们可以得到一些更明智的评论。
#2
I just had to fix this in my application. I did the following:
我只需要在我的应用程序中解决这个问题。我做了以下事情:
// variant delegate with variant event args
MyEventHandler<<in T>(object sender, IMyEventArgs<T> a)
// class implementing variant interface
class FiresEvents<T> : IFiresEvents<T>
{
// list instead of event
private readonly List<MyEventHandler<T>> happened = new List<MyEventHandler<T>>();
// custom event implementation
public event MyEventHandler<T> Happened
{
add
{
happened.Add(value);
}
remove
{
happened.Remove(value);
}
}
public void Foo()
{
happened.ForEach(x => x.Invoke(this, new MyEventArgs<T>(t));
}
}
I don't know if there are relevant differences to regular multi-cast events. As far as I used it, it works ...
我不知道常规多播事件是否存在相关差异。就我用它而言,它有效......
By the way: I never liked the events in C#. I don't understand why there is a language feature, when it doesn't provide any advantages.
顺便说一下:我从不喜欢C#中的事件。我不明白为什么有语言功能,当它没有提供任何优势。
#3
Are you getting the ArgumentException from both? If the exception is being thrown by just the new handler, then I would think that it's backward-compatible.
你从两者得到ArgumentException吗?如果仅通过新处理程序抛出异常,那么我认为它是向后兼容的。
BTW, I think you have your comments mixed up. In C# 3.0 this:
顺便说一句,我想你的意见混淆了。在C#3.0中:
button.Click += new EventHandler<EventArgs>(button_Click); // old
button.Click + = new EventHandler
wouldn't have run. That's c#4.0
不会跑。那是c#4.0