📜 ⬆️ ⬇️

Virtual events in C #: something went wrong

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; } //<= virtual 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; } } public override Action _myCustomEvent { get; set; } //<= override 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 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:


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; //<= var stopped = Stopped; //<= } .... } 

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, //<= new DesignItemCollectionEventArgs(items)); } } .... 

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(....); // <= } } .... public void BeforeComponentRemove(....) { .... if (OnBeforeComponentRemove != null) { OnBeforeComponentRemove(....); } } .... 

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; //<= if (afterDispose != null) afterDispose(this, EventArgs.Empty); } .... } 

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; //<= if (error != null) 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 .

Read the article and have a question?
Often our articles are asked the same questions. We collected answers to them here: Answers to questions from readers of articles about PVS-Studio, version 2015 . Please review the list.

Source: https://habr.com/ru/post/315600/


All Articles