C# - Event keyword advantages?

2019-01-17 00:26发布

问题:

I've come to recently understand that a C# 'event' really is. It isn't really anything, honestly. To sum up my findings: The event keyword is simply a modifier that only applies to delegates.

So, all the 'magic' of an event are the operations of a delegate. That's it. I've read through a lot of the Microsoft documentation, but there no sentence that summarizes in that manner so succinctly. To continue with my findings, delegate, class, and struct are all on the same 'level'. They are ways to define a 'object'. I don't mean 'object' as in the type, but an encapsulated concept of 'something'. Like how the word 'object' is used when saying object oriented programming.

Anyway, 'objects' have certain modifiers. For example, sealed, readonly, virtual, static, etc... This list can be found here. In the case of a delegate, it has the extra one called event. Event makes it so that when a delegate is declared as part of a class, it only exposes the add and remove methods as per the access modifier given to the event. These methods are defined in a similar nature to get and set of a property. Other operations of the delegate (assignment, read-access, method invocation, etc) are only allowed within the class in which the event delegate was declared. The other thing I find interesting, is that all delegates have the methods Invoke, BeginInvoke, and EndInvoke, yet you cannot navigate to view them within Visual Studio, nor could I find the documentation describing them...

Okay. So after knowing all that, what is the advantage of using the event keyword other than for modifying how the delegate can be accessed? It seems like, in many cases, I'd be better off simply declaring a delegate without the event keyword. A situation I ran into recently is that I wanted to create an abstract base class that contained 2 events. Any class that derived from this base class should be able to use the events like they are their own, similar to any other object of the class that is exposed to the derived class (aka, non-private, unless the derived class is in another assembly, and the object was declared internal).

Basically, I wanted the derived classes to use these events as their own. The only way to do this was to expose the backing variable of the events as protected, so the derived classes could raise the events. Looking at the code, this seemed pretty stupid since I was basically defining the delegate twice; once as a protected field, and the other as the public event. I thought,

Wouldn't I be better off making a class called Event that has an out parameter of an Action in the constructor? The action that is returned is equivalent to Raise that many have made as an extension method for delegates, where it checks to see if the delegate is null, and then invokes the delegate. The only public methods on Event would be Add and Remove for appending delegates and removing them from the underlying delegate (+=, -=). Classes could have these events as properties, such as,

public Event SomethingHappened { get; private set; }

so that only that class can re-assign the event. Or a public readonly field would be just as effective. The out parameter that is returned from the constructor is stored by the class and called when the class wants to raise the event. I know it is a hokey workaround, but it'll get the job done, and allows the events to not only be passed as arguments, but allow derived classes to call the Raise method if the base class defines it as protected.

TLDR:

What is the advantage of using the event keyword other than for modifying how the delegate can be accessed?

回答1:

What is the advantage of using the event keyword other than for modifying how the delegate can be accessed?

That is the primary advantage of using the event keyword. You use an event over just a raw delegate to prevent the delegate from being invoked or cleared from outside the scope of the class it is defined in, because in the case of events it is the responsibility of that class to invoke the event. External entities shouldn't be invoking it directly (they can and should be invoking the event indirectly), nor should they "care" about whether there are any other event handlers or be involved in touching them (by, for example, assigning an entirely new delegate to the field).

The specific case of wanting to allow sub-classes to fire the event is most commonly solved by having the class that defines the event creating a protected method that does nothing but fire the event. Such methods will, by convention, have the same name as the event but with "On" prefixing it.

Yes, you could create your own type that logically represents an event, is a wrapper for a delegate, and limits the functions that can be performed on that event to those that "should" be able to perform them (possibly using slightly different rules than the C# event keyword uses. This is something that is frequently used in other languages that don't have an event keyword (or possibly even delegates). The C# designers simply realized that this was a very common pattern, and felt that it was worth the energy to add the keyword to the language to help minimize the boilerplate code required to create a logical "event".

Another benefit of using the event keyword, as opposed to just having some type of delegate as a property, is that you make your intentions much clearer. If I see just a delegate property the implication is generally that it represents one method. Yes, all delegates in C# are multicast delegates, so that's not true, but it's unusual for people to leverage that functionality outside of events. People think that an Action represents one action, not a list of actions. Events also have special treatment with respect to the C# documentation. They are all listed separately, they have different icons in visual studio, etc. This all helps make the intentions and semantics of the member much clearer to someone using the class at a glance.

Finally, the event keyword ensures that there is synchronization between multiple threads, which isn't performed by the Delegate class. If multiple threads go to add handlers to an event at the same time, the event keyword ensures both are added. If you just publicly expose a delegate it's possible for one to overwrite the other due to a race condition and have one handler end up dropped on the floor. If you roll your own Event class you could provide this functionality, but it is both more boilerplate code and something that's pretty darn easy to mess up (either resulting in leaving race conditions in, or excessive synchronization resulting in lost performance).



回答2:

Basically, I wanted the derived classes to use these events as their own. The only way to do this was to expose the backing variable of the events as protected, so the derived classes could raise the events.

The usual way to handle this is not to expose the field, but to expose a method to raise the event.

public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) {
  var handler = PropertyChanged;
  if (handler != null)
    handler(this, e);
}

This not only allows derived classes to raise the event, but also allows derived classes to do something before any subscribed handlers actually get called.

To answer your actual question, though:

What is the advantage of using the event keyword other than for modifying how the delegate can be accessed?

One advantage not yet mentioned (I think):

public event PropertyChangedEventHandler PropertyChanged;

can be changed to

public event PropertyChangedEventHandler PropertyChanged
{
  add { /* custom code here */ }
  remove { /* custom code here */ }
}

without requiring a recompile of all users of your library. You might want something like this if you later find a reason to not simply store the handlers in a private field. This is the same advantage of an auto-implemented property over a field.



回答3:

I think you can easly compare 'event' keyword to accessors but you have some other benefits coming from 'event' keyword:

  • easier to read, everyone knows what 'event' means
  • less work, you don't need to create special functions to subscribe and unsubscribe from delegate variable
  • adding to or removing from delegate that has 'event' keyword has 'lock' (synchronization) added.
  • IDE / frameworks can interpret 'events' and help you