다음을 통해 공유


Virtual Events in C#

Technorati Tags: Events

One of the things that the language designers considered when designing the C# language was the ability to notify external callers of certain events happening. To solve this problem, they (surprise surprise) introduced the event construct.

One of the oddities in the design however, comes in the form of virtual events. This is one of those design decisions that we recognize is something we would like to change, but as my colleague, Eric Lippert explains in a series of posts concerning breaking changes, we aren't able to fix everything that we would like to.

How virtual events work

So first off, lets quickly describe how virtual events work. Section 10.8.4 of the spec describes this for us:

A virtual event declaration specifies that the accessors of that event are virtual. The virtual modifier applies to both accessors of an event.
The accessors of an inherited virtual event can be overridden in a derived class by including an event declaration that specifies an override modifier. This is known as an overriding event declaration. An overriding event declaration does not declare a new event. Instead, it simply specializes the implementations of the accessors of an existing virtual event.

Notice that an overriding event does not declare a new event. Let's now quickly refresh field-like events and consider how they work in conjunction with virtual and overriding events. They are described in 10.8.1:

Within the program text of the class or struct that contains the declaration of an event, certain events can be used like fields. To be used in this way, an event must not be abstract or extern, and must not explicitly include event-accessor-declarations. Such an event can be used in any context that permits a field. The field contains a delegate (§15) which refers to the list of event handlers that have been added to the event. If no event handlers have been added, the field contains null.

Declaring a virtual field-like event then, will cause the compiler to generate a delegate field to back the event, and virtual accessors for the event. Declaring an overriding field-like event will not cause the compiler to generate a new backing field for the overriding event in the case of field-like events, but will cause it to generate overriding accessors.

A concrete example of virtual and overriding field-like events

Now, consider some parent class P which declares a virtual event, and some derived class D which overrides it. First, note that we have four combinations:

  1. P declares a field-like event, and D declares a field-like event
  2. P declares a field-like event, and D declares a user-defined event
  3. P declares a user-defined event, and D declares a field-like event
  4. P declares a user-defined event, and D declares a user-defined event

In case (1), P contains a delegate field backing the event, and two virtual accessors that add and remove from the delegate. D contains two overriding accessors, who add and remove from the delegate contained in P. Notice that for this to work, the delegate in P must be elevated from private to protected.

In case (2), P contains a delegate field and two accessors, and D contains the two user-defined overloaded accessors. D does not have access to the field contained in P.

In case (3), P contains the two virtual user-defined accessors, and D contains a delegate field, along with two overriding accessors that add and remove from the backing field.

In case (4), P contains the two virtual user-defined accessors, and D contains the two overriding user-defined accessors.

Here's the code for it:

 class P
{
    public virtual event EventHandler case1_event;
    public virtual event EventHandler case2_event;
    public virtual event EventHandler case3_event
    {
        add { }
        remove { }
    }
    public virtual event EventHandler case4_event
    {
        add { }
        remove { }
    }
}

class D : P
{
    // D has access to P.case1_event's backing field.
    public override event EventHandler case1_event;

    // D does not have access to P.case2_event's backing field.
    public override event EventHandler case2_event
    {
        add { }
        remove { }
    }

    // D has a backing field generated for case3_event, which is private
    public override event EventHandler case3_event;

    // This is just the typical virtual/override pattern.
    public override event EventHandler case4_event
    {
        add { }
        remove { }
    }
}

A bug in the compiler

The current C# compiler (in Visual Studio 2008 Beta2) has a bug when dealing with scenario (1) above. Consider the following scenario:

 class P
{
    public virtual event EventHandler myEvent;
    public void parentEventCall()
    {
        myEvent(this, null);
    }
}
class D : P
{
    public override event EventHandler myEvent;
    public void derivedEventCall()
    {
        myEvent(this, null);
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Create an instance of D, and create a P reference to it.
        D derived = new D();
        P parent = derived;

        // Hook a handler up through the derived and parent references.
        derived.myEvent += new EventHandler(derivedHandler);
        parent.myEvent += new EventHandler(parentHandler);

        // Fire both events.
        derived.derivedEventCall();
        parent.parentEventCall();

    }
    static void parentHandler(object sender, EventArgs e)
    {
        Console.WriteLine("parent handler");
    }

    static void derivedHandler(object sender, EventArgs e)
    {
        Console.WriteLine("derived handler");
    }
}

Now, we would expect this code to output the sequence of derived/parent/derived/parent handlers being fired, because in this scenario, we should have one backing field for both the virtual event and the overriding one, so both of the trigger calls should act upon the same event, and both of the handlers should be hooked onto the same event.

However, if you go ahead and paste this code into Visual Studio, compile it, then run it, you'll get a NullReferenceException being thrown. Debugging it will show us two thing:

Firstly, the compiler does not generate a protected backing field on the parent class P. Instead, it generates two private backing fields - one in P and one in D.

Secondly, when we execute both handler hookups, we'll see that it is the derived delegate field that gets both of the handlers hooked up to it.

When we step into parentEventCall then, we'll notice that the this pointer is of type P, and that the only visible backing field is P.myEvent, which is null. Attempting to trigger the delegate then throws us the NullReferenceException, as expected.

The work-around

The simple work-around for the issue is to use a virtual method for triggering the event as well. An easy to remember rule of thumb is that if you have a virtual event, have a virtual triggering method. If you override a virtual event, override its trigger method as well.

A quick argument for not-fixing this issue

There are two main reasons that prompted us to choose to not fix this issue.

The first is the common practice for declaring event trigger methods. Whenever you write a trigger method, you must do a null check - if you don't and someone calls your trigger method without ever adding a handler, you'll get a NullreferenceException. This means that anyone applying good coding practices will already be safeguarded from this erroneous exception being thrown.

However, fixing this issue will be a breaking change. Code that used to never execute because the backing field in the parent class was always null will now execute because the backing field is the same in both the parent and derived classes, and will have a value once a handler is added. This is undesirable.

Secondly, there really is no need to be in this scenario in the first place. Using a virtual field-like event in the parent class and not changing any of its behavior in the overriding derived class is not necessary. You can simply omit the override in the derived class to get the desired behavior.

kick it on DotNetKicks.com

Comments

  • Anonymous
    November 26, 2007
    You've been kicked (a good thing) - Trackback from DotNetKicks.com

  • Anonymous
    January 12, 2008
    I'm impressed how detailed you study the problem of virtual events and their possible shortcomings, although I doubt that many people would encounter that problem. A much more pressing design problem of the C# language is the lack of weak events. Presently, if an event listener registers an event handler with an event source, the event source will have a strong reference to the event listener. This can be a serious problem and many developers are even not aware of it. They create a new form, add the event handler, use the form and finally close the form. They assume the form will be garbage collected, but this is not the case, because they forgot to unregister the eventhandler and there is still a strong reference pointing to the form. Unfortunately, there are situation were it is not that easy to decide when to unregister from an event source. Just ask teh WPF team for examples. That the event listener should have the same life span as the event source is completely counter intuitive. And even if the developer is aware of that problem and wants to solve it, .NET doesn't offer him any help to do so. I spent weeks trying to get in run efficiently, writing a WeakEventManager as recommended in some blogs. Even Microsoft's own WPF team had to go this way and ended up with a horribly over complicated solution, the WeakEventManager, which can only be used for WPF applications. And what can the rest of us use ? It would be so easy to add a second kind of events, which use a weak reference pointer to the event listener. All that needs to be done is that the garbage collector consider the reference as weak. Trying to fix this design problem of C# is extremly hard to do for a developer and many highly qualified programmers have failed or come up with terribly complicated, inefficient solutions, like generating code during runtime. I'm sure you guys would get much more appreciation if you would provide a solution for weak events instead of virtual events.

  • Anonymous
    June 06, 2011
    Actually, I have a situation where I need to override a base field-type event with a derived field-type event.  I've written an article on what I'm doing here: www.codeproject.com/.../refeventdelegates.aspx I need to decorate my events with an attribute that contains derived class information.  But, my events are actually defined in an interface that a implemented in a base class (for convenience).  There's no way to override just the attribute arguments from the base class's event declaration.  So I have defined them as virtual and override them simply to be able to attach a new field attribute.  Is this a valid approach, or is there another way to do what I'm doing?  Will I get hit with the compiler bug?  (I'm using VS2008)