The contravariance of events and delegates in .NET 4.0 and C # 4.0

Studying this issue , I became interested in how this will affect the new possibilities of covariance / contravariance in C # 4.0.

In beta 1, C # does not seem to agree with the CLR. Return to C # 3.0 if you have:

public event EventHandler<ClickEventArgs> Click; 

... and then in another place:

 button.Click += new EventHandler<EventArgs>(button_Click); 

... the compiler will be barf, because these are incompatible delegate types. But in C # 4.0, it compiles fine, because in CLR 4.0 the type parameter is now marked as in , therefore it is contravariant, and therefore the compiler assumes that the multicast delegate += will work.

Here is 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 does not work at runtime ( ArgumentException : delegates must be of the same type).

This is fine if you add only one of the two types of delegates. But a combination of two different types in multicast throws an exception when a second is added.

I assume this is a bug in the CLR in beta 1 (the compiler behavior looks, hopefully, correct).

Update for Release Candidate:

The above code no longer compiles. It must be that TEventArgs comparability in the EventHandler<TEventArgs> delegate type is rollback, so now the delegate has the same definition as in .NET 3.5.

That is, the beta I was looking at should have:

 public delegate void EventHandler<in TEventArgs>(object sender, TEventArgs e); 

Now it goes back to:

 public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e); 

But the delegate parameter Action<T> T is still contravariant:

 public delegate void Action<in T>(T obj); 

The same can be said for Func<T> T being covariant.

This trade-off makes a lot of sense if we assume that the initial use of multicast delegates is in the context of events. I personally found that I never use multicast delegates other than events.

So, I believe that C # coding standards can now accept a new rule: do not form multicast delegates from several types of delegates related to covariance / contravariance. And if you don't know what that means, just avoid using Action for events that are on the safe side.

Of course, this conclusion matters to the original question that this sprout grew from ...

+25
c # events delegates
Jul 13 '09 at 16:47
source share
3 answers

Very interesting. You do not need to use events to make this happen, and I find it easier to use simple delegates.

Consider Func<string> and Func<object> . In C # 4.0, you can implicitly convert Func<string> to Func<object> , because you can always use a string reference as an object reference. However, when you try to combine them, everything goes wrong. Here's a short but complete program demonstrating the problem in two different ways:

 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 Combine calls (hidden by syntactic sugar + =) throw exceptions. (Comment on the first to see the second).

This is definitely a problem, although I'm not quite sure which solution should be. It is possible that at run time, the delegate code will need to develop the most appropriate type to use based on the delegate types involved. This is a little nasty. It would be nice to have a generic Delegate.Combine call, but you could not really express the corresponding types in a meaningful way.

One thing worth noting is that the covariant transform is a reference transform - in the above, multi1 and stringFactory refer to the same object: this is not the same as writing

 Func<object> multi1 = new Func<object>(stringFactory); 

(At this point, the next line will be executed without exception). At run time, BCL really deals with combining Func<string> and Func<object> ; he has no other information to continue.

This is unpleasant, and I seriously hope that it will be fixed in some way. I warned Mads and Eric about this issue so we can get a more informed comment.

+9
Jul 13 '09 at 19:03
source share

I just had to fix this in my application. I have done 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 do not know if there are significant differences in regular events with multiple sheets. As far as I used, it works ...

By the way: I never liked events in C # . I do not understand why there is a language function when it does not give any advantages.

+1
Oct 05
source share

Are you getting an ArgumentException from both? If the exception throws only a new handler, then I think it is backward compatible.

By the way, I think your comments are mixed up. In C # 3.0, this is:

button.Click += new EventHandler<EventArgs>(button_Click); // old

did not start. This is C # 4.0

0
Jul 13 '09 at 18:24
source share



All Articles