Partager via


Events get a little overhaul in C# 4, Part III: Breaking Changes

In the last two posts, I revealed that field-like events in C# 4 have a better synchronization story, and that we changed += and -= in a kind of subtle way to protect you, in many cases, from the semantic differences this introduces between C# 3 and C# 4.

Now I’m here to tell you about some more in-your-face breaks that are a lot less subtle, and related to the binding change for += and -=. My hope is that anyone who actually sees these breaks will find this post and learn how to fix them. Let’s go.

Break #1: warning CS0067: The event 'MyClass.MyEvent' is never used

So imagine you have the following C# 3 code. It compiles just fine. No warnings, no errors. (Forget the about the actual utility of this piece of code for a moment).

 using System;

class MyClass
{
    static void Main()
    {
        MyClass x = new MyClass();
        x.MyEvent += () => Console.WriteLine("Yay!");
    }
    public event Action MyEvent;
}

You upgrage to C# 4 and now you get a warning!

 test.cs(10,25): warning CS0067: The event 'MyClass.MyEvent' is never used

What happened? Well, remember the subtle change in the way we interpreted += from the last post? It affects line 8 above. In C# 3, since this code is inside the class that defines the event, the += was a compound operator. Therefore it was a direct operation on the delegate field that backs MyEvent. In C# 4, however, we’ve changed the interpretation so that the += on line 8 is now calling the add accessor instead.

So why the warning? Because the warning is trying to tell you something about the private backing field, not the event accessors! In C# 4, this class has a delegate field that no one ever uses for anything. No one invokes it, no one passes it to Delegate.Combine, no one does anything with it. Of course the event accessors use it, but compiler generated code doesn’t count for this warning (and it never has).

The first possible fix here is: make use of the event! Perhaps you meant to raise the event, but you never got around to adding the code, and now this warning is actually telling you about a bug. If you were to add an “OnMyEvent” method that invoked MyEvent with “MyEvent()”, then you’ve made use of the field, the warning goes away, and you’ve fixed a bug.

The second fix is to get rid of the event, or at least the backing field. In this case, you don’t actually want to raise the event, and you might as well delete the thing. So you can safely remove the event and all the references to it and all’s well. It didn’t do anything anyway.

Of course, you might want to delete the event but be unable to. This can be the case if some base class or interface that you implement requires you to define the event. In that case, you probably have code like this:

 interface IHasSomeEvent
{
    event Action SomeEvent;
}

class MyClass : IHasSomeEvent
{
    // there is a backing delegate here that goes unused!
    public event Action SomeEvent;
}

and my recommendation would be that you change your class to look like this:

 class MyClass : IHasSomeEvent
{
    // this takes no storage and does nothing!
    event Action IHasSomeEvent.SomeEvent { add { } remove { } } 
}

This last case, where you are obligated to implement some event that you don’t care about, is the context in which I’ve seen these warning 67’s pop up most frequently. Notice that the fix I recommend actually does nothing more than fulfill the obligation of implementing the interface, and it doesn’t pollute the public surface area of your class with the event name. If you wanted to be especially hard-nosed, you could throw a NotImplementedException from those accessors, but that seems like a bit much to me.

Break #2: error CS0029: Cannot implicitly convert type 'void' to 'System.Action'

This break can also show up as “CS1503: Argument n: cannot convert from 'void' to 'System.Action'” or any of a few different errors where a conversion is required but does not exist. And it comes from code like this:

 class MyClass
{
    event Action MyEvent;
    public void DoSomething()
    {
        Action a = MyEvent += () => Console.WriteLine("Yay!");
        a();
    }
}

Here, what we’re doing is adding the little Console.WriteLine delegate to my event, and then call the result. This works in C# 3, again, because += binds directly to the event. Therefore, the result of the compound assignment is the new delegate that is now referenced by MyEvent. However, in C# 4, since += is a call to an event add accessor, and the result of an event accessor call is void, you cannot directly look at the result like this. Event accessors are designed this way so that code outside your class can’t break encapsulation and get their hands on your delegates.

This is really weird code. I have never actually seen it in the wild. If you have such code, why not just call the accessor and then call the event?

 class MyClass
{
    event Action MyEvent;
    public void DoSomething()
    {
        MyEvent += () => Console.WriteLine("Yay!");
        MyEvent();
    }
}

Or, generally, call the accessor and then do whatever you were going to do with the result but with the event instead? The semantics are not 100% the same if you do this (for instance, someone on another thread could have modified the event in the meantime), but if you think that makes a difference to you, then you should re-examine your requirements and possibly implement the event accessors yourself because the original code was wrong (if you left out the lock(this)) or terribly unsafe (if you used lock(this)).

Other meaningless behavioral differences

There are a few other things that are different now that your +=’s are all calls to an accessor. But none that you should spend any time whatsoever worrying about; I list them here only to be thorough.

For instance, there’s another method call here. That could consume extra stack space. If you had just achieved the limit on your stack before, this one extra method call could push you over the edge and give you a StackOverflowException.

Also, there is a theoretical resource starvation problem with the compare and swap lock-free code when two threads sort of “line up” perfectly forever. This will affect you with probability zero.

And you could do silly things, such as: put a SecurityCriticalAttribute on your field-like event accessors (use the “method:” attribute target specifier), purely for the purpose of creating a situation wherein you can run in partial trust and a += in the event-defining class used to work in C# 3 but now it does not. Just... don’t do this.

Next time I’ll conclude this short series by going over the standard event pattern in C# 4, with recommendations about exactly how you should implement and use field-like events, and when you should not.

Comments

  • Anonymous
    March 18, 2010
    Hi Chris, What about the breaking changes that compiler won't detect? I mean lets say someone relied on the implementation of accessors to lock on this?.for example someone relied that in the code like thislock(this){  delegateA();  delegateA();   // or actually any other delegateB()}Code could have relied on the fact that the delegate doesn't change while under lock.
  • Anonymous
    March 19, 2010
    The comment has been removed
  • Anonymous
    March 22, 2010
    I was not saying that there is no way to change the underlying event.I was arguing that there could be a case (another thread in my example) that modifies E1/E2 was not able to do it while thread 1 was holding the lock.
  • Anonymous
    March 23, 2010
    Chris, any idea if there will ever be support for weak delegates in the CLR?At the moment there's no easy way to implement an Observer pattern.  People like IanG have tried to cobble something together and failed.The only workarounds seem to require support from the observed object, and require a tremendous about of code.  The WeakEvent pattern in WPF is worthless for any code not pumping a dispatch queue.Where are weak delegates?  For that matter, what has the CLR team been doing since 2.0 came out, five years ago?There's been an issue (ID 94154) on MS Connect about this since 2004, and still no word from Microsoft on when (if?) it's going to be fixed.