
Recently, I was working on a new C # -diagnostics of V3119 for the PVS-Studio static analyzer. The purpose of diagnostics is to identify potentially unsafe constructions in C # source code associated with the use of virtual and redefined events. Let's try to figure out what is wrong with virtual events in C #, how exactly diagnostics work, and why Microsoft does not recommend using virtual and redefined events?
Introduction
I think the reader is familiar with the virtuality mechanisms in C #. The easiest to understand is the example of how virtual methods work. In this case, virtuality allows you to perform one of the overrides of the virtual method in accordance with the
type of object
execution time . I will illustrate this mechanism with a simple example:
class A { public virtual void F() { Console.WriteLine("AF"); } public void G() { Console.WriteLine("AG"); } } class B : A { public override void F() { Console.WriteLine("BF"); } public new void G() { Console.WriteLine("BG"); } } static void Main(....) { B b = new B(); A a = b; aF(); bF(); aG(); bG(); }
As a result of execution, the console will display:
')
BF BF AG BG
All right Since both objects
a and
b have the
runtime type B , calling the virtual method
F () for both of these objects will result in calling the redefined method
F () of class
B. On the other hand, by the
type of compile time, objects
a and
b are different, respectively, having types
A and
B. Therefore, calling the
G () method for each of these objects results in calling the appropriate method for class
A or
B. More details about the use of the
virtual and
override keywords can be found, for example,
here .
Similarly to methods, properties and indexers,
events can also be declared virtual:
public virtual event ....
This can be done both for "simple" and for explicitly implementing accessors
add and
remove events. At the same time, working with virtual events and redefined in derived classes, it would be logical to expect from them behavior similar to, for example, virtual methods. However, it is not. Moreover,
MSDN in plain text
does not recommend the use of virtual and redefined events: “Do not declare events. It is unpredictable whether it’s going to be a subscribing to the base class event.
But we will not immediately give up and try to still implement the "... declare virtual classes".
Experiments
As a first experiment, we will create a console application containing the declaration and use of two virtual events in the base class (with implicit and explicit implementations of the
add and
remove accessors), and also containing a derived class that overrides these events:
class Base { public virtual event Action MyEvent; public virtual event Action MyCustomEvent { add { _myCustomEvent += value; } remove { _myCustomEvent -= value; } } protected Action _myCustomEvent { get; set; } public void FooBase() { MyEvent?.Invoke(); _myCustomEvent?.Invoke(); } } class Child : Base { public override event Action MyEvent; public override event Action MyCustomEvent { add { _myCustomEvent += value; } remove { _myCustomEvent -= value; } } protected new Action _myCustomEvent { get; set; } public void FooChild() { MyEvent?.Invoke(); _myCustomEvent?.Invoke(); } } static void Main(...) { Child child = new Child(); child.MyEvent += () => Console.WriteLine("child.MyEvent handler"); child.MyCustomEvent += () => Console.WriteLine("child.MyCustomEvent handler"); child.FooChild(); child.FooBase(); }
The output of the program will be the output to the console of two lines:
child.MyEvent handler child.MyCustomEvent handler
Using a debugger or test output, it is easy to make sure that when
child.FooBase () is called, the value of both
MyEvent and
_myCustomEvent variables is
null, and the program does not "fall" only by using the conditional access operator when trying to trigger
MyEvent? .Invoke events ( ) and
_myCustomEvent? .Invoke () .
So the MSDN warning was not in vain. It really does not work! Subscribing to virtual events of an object that has the runtime type of the derived class
Child does not lead to a simultaneous subscription to the events of the base class
Base .
In the case of an implicit event implementation, the compiler automatically creates accessor methods for it
add and
remove , as well as a delegate field that is used for subscription or unsubscribe. The problem, apparently, is that in case of using a virtual event, the base and child classes will have individual (non-virtual) delegate fields associated with this event.
In the case of an explicit implementation, this is done by the developer, who can take into account this feature of the behavior of virtual events in C #. In the above example, I did not take into account this feature by declaring the
_myCustomEvent delegate
property as
protected in the base and derived classes. Thus, I actually repeated the implementation provided by the compiler automatically for virtual events.
Let us try to achieve the expected behavior of the virtual event with the help of the second experiment. To do this, we use a virtual and redefined event with an explicit implementation of the accessors
add and
remove , as well as the associated
virtual property delegate. Let's slightly change the program text from the first experiment:
class Base { public virtual event Action MyEvent; public virtual event Action MyCustomEvent { add { _myCustomEvent += value; } remove { _myCustomEvent -= value; } } public virtual Action _myCustomEvent { get; set; }
The result of the program:
child.MyEvent handler child.MyCustomEvent handler child.MyCustomEvent handler
Pay attention to the fact that two handlers occurred for the
child.MyCustomEvent event. In debug mode, it is easy to determine that now when calling
_myCustomEvent? .Invoke () in the
FooBase () method, the delegate
_myCustomEvent value is not
null . Thus, we only managed to achieve the expected behavior for virtual events by using events with explicitly implemented accessors
add and
remove .
You will say that all this, of course, is good, but we are talking about some synthetic examples from the theoretical field, and let these virtual and redefined events remain there. I will give the following comments:
- You may be in a situation where you will be forced to use virtual events. For example, inheriting from an abstract class in which an abstract event with an implicit implementation is declared. As a result, you will receive in your class a redefined event that you may be using. There is nothing dangerous in this until the moment you decide to inherit from your class and redefine this event again.
- Such designs are rare, but still occur in real projects. I made sure of this after I implemented the C # -diagnosis of V3119 for the PVS-Studio static analyzer. The diagnostic rule searches for declarations of virtual or redefined events with implicit implementation that are used in the current class. It is considered unsafe that such constructions are found and the class may have heirs, and the event can be redefined (not sealed ). That is, when a situation is hypothetically possible with the redefinition of a virtual or already redefined event in a derived class. The warnings found in this way are listed in the next section.
Examples from real projects
To test the performance of the PVS-Studio analyzer, we use a pool of test projects. After adding a new rule V3119, dedicated to virtual and redefined events, to the analyzer, the entire project pool was checked. Let us analyze the warnings received.
Roslyn
Previously,
an article was devoted to checking this project with PVS-Studio. Now I’ll just give you a list of analyzer warnings related to virtual and redefined events.
PVS-Studio analyzer warning: V3119 Calling overridden event 'Started' may lead to unpredictable behavior. Consider implementing event accessors explicitly or use the 'sealed' keyword. GlobalOperationNotificationServiceFactory.cs 33
PVS-Studio analyzer warning :
V3119 Calling overridden event 'Stopped' may lead to unpredictable behavior. Consider implementing event accessors explicitly or use the 'sealed' keyword. GlobalOperationNotificationServiceFactory.cs 34
private class NoOpService : AbstractGlobalOperationNotificationService { .... public override event EventHandler Started; public override event EventHandler<GlobalOperationEventArgs> Stopped; .... public NoOpService() { .... var started = Started;
In this case, we most likely deal with the situation of the forced redefinition of virtual events. The base class
AbstractGlobalOperationNotificationService is abstract and contains the definition of the
Started and
Stopped abstract events:
internal abstract class AbstractGlobalOperationNotificationService : IGlobalOperationNotificationService { public abstract event EventHandler Started; public abstract event EventHandler<GlobalOperationEventArgs> Stopped; .... }
Further use of the overridden
Started and
Stopped events is not entirely clear, since the delegates are simply assigned to the local variables
started and
stopped in the
NoOpService method and are not used at all. Nevertheless, this situation is potentially unsafe, which the analyzer warns about.
SharpDevelop
The verification of this project was also previously described in the
article . I will give a list of received alerts V3119 analyzer.
PVS-Studio analyzer warning :
V3119 Calling overridden event 'ParseInformationUpdated' may lead to unpredictable behavior. Consider implementing event accessors explicitly or use the 'sealed' keyword. CompilableProject.cs 397
.... public override event EventHandler<ParseInformationEventArgs> ParseInformationUpdated = delegate {}; .... public override void OnParseInformationUpdated (....) { .... SD.MainThread.InvokeAsyncAndForget (delegate { ParseInformationUpdated(null, args); });
Detected using overridden virtual event. The danger will trap us in the case of inheritance from the current class and override the
ParseInformationUpdated event in a derived class.
PVS-Studio analyzer warning: V3119 Calling overridden event 'ShouldApplyExtensionsInvalidated' event may lead to unpredictable behavior. Consider implementing event accessors explicitly or use the 'sealed' keyword. DefaultExtension.cs 127
.... public override event EventHandler<DesignItemCollectionEventArgs> ShouldApplyExtensionsInvalidated; .... protected void ReapplyExtensions (ICollection<DesignItem> items) { if (ShouldApplyExtensionsInvalidated != null) { ShouldApplyExtensionsInvalidated(this,
Re-detected use of overridden virtual event.
Space engineers
And this project was previously tested with PVS-Studio. The result of the check is given in the
article . New diagnostics V3119 issued 2 warnings.
PVS-Studio analyzer warning: V3119 Calling virtual event 'OnAfterComponentAdd' may lead to unpredictable behavior. Consider Implementing event accessors explicitly. MyInventoryAggregate.cs 209
PVS-Studio analyzer warning: V3119 Calling virtual event OnBeforeComponentRemove 'may lead to unpredictable behavior. Consider Implementing event accessors explicitly. MyInventoryAggregate.cs 218
.... public virtual event Action<MyInventoryAggregate, MyInventoryBase> OnAfterComponentAdd; public virtual event Action<MyInventoryAggregate, MyInventoryBase> OnBeforeComponentRemove; .... public void AfterComponentAdd(....) { .... if (OnAfterComponentAdd != null) { OnAfterComponentAdd(....);
Here we are dealing with declaring and using not redefined, but virtual events. In general, the situation is no different from those previously discussed.
RavenDB
The RavenDB project is a so-called “NoSQL” (or document-oriented) database. Its detailed description is available on the
official website . The project is developed using .NET and its source codes are available on
GitHub . Checking RavenDB with the PVS-Studio analyzer revealed three warnings V3119.
PVS-Studio analyzer warning: V3119 Calling overridden event 'AfterDispose' may lead to unpredictable behavior. Consider implementing event accessors explicitly or use the 'sealed' keyword. DocumentStore.cs 273
PVS-Studio analyzer warning: V3119 Calling overridden event 'AfterDispose' may lead to unpredictable behavior. Consider implementing event accessors explicitly or use the 'sealed' keyword. ShardedDocumentStore.cs 104
Both of these warnings are issued for similar code fragments. Consider one of these fragments:
public class DocumentStore : DocumentStoreBase { .... public override event EventHandler AfterDispose; .... public override void Dispose() { .... var afterDispose = AfterDispose;
The
AfterDispose event
overridden in the
DocumentStore class is declared as abstract in the
DocumentStoreBase base abstract class:
public abstract class DocumentStoreBase : IDocumentStore { .... public abstract event EventHandler AfterDispose; .... }
As in the previous examples, the analyzer warns us of potential danger if the
AfterDispose virtual event is redefined and used in the
DocumentStore- derived classes.
PVS-Studio analyzer warning: Calling virtual event 'Error' may lead to unpredictable behavior. Consider Implementing event accessors explicitly. JsonSerializer.cs 1007
.... public virtual event EventHandler<ErrorEventArgs> Error; .... internal void OnError(....) { EventHandler<ErrorEventArgs> error = Error;
Here is the announcement and use of a virtual event. Again, there is a risk of undefined behavior.
Conclusion
I think this is the end of our research and the conclusion that you really should not use implicitly implemented virtual events. Due to the nature of their implementation in C #, the use of such events can lead to undefined behavior. In case you still have to use redefined virtual events (for example, when inheriting from an abstract class), this should be done with caution using explicitly given
add and
remove accessors. You can also use the
sealed keyword when declaring a class or event. And, of course, you should use static code analysis tools, for example,
PVS-Studio .
If you want to share this article with an English-speaking audience, then please use the link to the translation: Sergey Khrenov.
Virtual events in C #: something went wrong .