Tuesday, August 19, 2008

Delegates and events. What does `event` keyword actually mean in C#

In a very interesting "Accelerated C# 2008" book, its author Trey Nash described the role of C# events (page 262):
"... .NET runtime designers were so generous as to define a formalized built-in event mechanism. When you declare an event within a class, internally the compiler implements some hidden methods that allow you to register and unregister delegates, which are called when a specific event is raised. In essence, an event is a shortcut that saves you the time of having to write the register and unregister methods that manage a delegate chain yourself."
Then, on a page 265 he is saying:
"... events are ideal for implementing a publish/subscribe design pattern, where many listeners are registering for notification (publication) of an event. Similarly, you can use .NET events to implement a form of the Observer pattern, where various entities register to receive notifications that some other entity has changed."

Often, when some material is not clear to me, I try to read another book to clarify a matter. So, I found the following description of events in "C# 3.0 in a Nutshell" book (page 112:
"When using delegates, two emergent roles commonly appear: broadcaster and subscriber.
The broadcaster is a type that contains a delegate field. The broadcaster decides when to broadcast, by invoking the delegate.
The subscribers are the method target recipients. A subscriber decides when to start and stop listening, by calling += and -= on the broadcaster's delegate. A subscriber does not know about, or interfere with, other subscribers.
Events are a language feature that formalizes this pattern. An event is a wrapper for a delegate that exposes just the subset of delegate features required for the broadcaster/subscriber model. The main purpose of events is to prevent subscribers from interfering with each other."

This is quite opposite to what Trey said about events! They are introduced to prevent subscribers from interfering with each other, not as a shortcut that saves you the time of having to write the register and unregister methods.
+= and -= operators are defined for non-event delegates as well as for event delegates, while non-event delegates also expose assignment (=) operator and GetInvocationList() method which are too dangerous for subscribers. They allow a particular subscriber to investigate which other classes also subscribed to the same delegate and even allow to remove other independent subscribers. That's exactly what we prevent by adding an `event` keyword to a delegate.

Here's a modified code snippet from "C# 3.0 in a Nutshell" which proves what I said above (note, that it does not use famous EventHandler<TEventArgs> delegates; events could actually be used with any delegates):


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Chp04_events {
 
  internal delegate void PriceChangedHandler( decimal oldPrice, decimal newPrice );
 
  /// <summary>
  /// "Broadcaster" class
  /// </summary>
  internal class Stock {
    string _symbol;
    decimal _price;
 
    public Stock( string symbol ) { this._symbol = symbol; }
 
    // If you comment out this line and uncomment next line,  "myStock.PriceChanged = ..." and "Delegate[] chain = " statements
    // below in public void SetStockToWatch( Stock myStock ) methods would become legal, compromising an independence of event subscribers.
    // That's why we use an `event` keyword, rather than simpli a delegate.
    public event PriceChangedHandler PriceChanged;
    //public PriceChangedHandler PriceChanged;
 
    public decimal Price {
      get { return _price; }
      set {
        if (_price == value) return;
        if (PriceChanged != null) {
          // Code within the Broadcaster type has full access to event and can treat it as delegate:
          Delegate[] chain = PriceChanged.GetInvocationList();
          Console.WriteLine( "There is/are {0} subscribers watching for event I'm broadcsting", chain.Length );
          Console.WriteLine( "The first subscriber has a type of {0}", chain[0].Target.GetType() );
          if (chain.Length > 1) {
            Console.WriteLine( "The second subscriber has a type of {0}", chain[1].Target.GetType() );
          }
 
          PriceChanged( _price, value );
        }
        _price = value;
      }
    }
  }
 
  /// <summary>
  /// "Subscriber" class
  /// </summary>
  internal class StockWatcher {
    protected string _watcherName = String.Empty;
 
    public StockWatcher( string watcherName ) {
      this._watcherName = watcherName;
    }
 
    public string WatcherName {
      get { return _watcherName; }
    }
 
    public void SetStockToWatch( Stock myStock ) {
      myStock.PriceChanged += new PriceChangedHandler( myStock_PriceChanged );
      // Code outside the Broadcaster type can only perform += an -=. "Subscriber" does not know about other "subscribers".
      // The following line of code won't compile: 
      // Error: The event 'Chp04_events.Stock.PriceChanged' can only appear on the left hand side of += or -=
      // (except when used from within the type 'Chp04_events.Stock')
      myStock.PriceChanged = new PriceChangedHandler( myStock_PriceChanged );
      // The same error:
      Delegate[] chain = myStock.PriceChanged.GetInvocationList();
    }
 
    public void myStock_PriceChanged( decimal oldPrice, decimal newPrice ) {
      Console.WriteLine( "{0}: Stock price was changed from {1} to {2}", WatcherName, oldPrice, newPrice );
    }
  }
 
  internal class NewStockWatcher : StockWatcher {
    public NewStockWatcher( string watcherName ) : base( watcherName ) { }
  }
 
  class Program {
    static void Main( string[] args ) {
      decimal myDecPrice;
 
      var myStock = new Stock( "Napster" );
      var myStockWatcher1 = new StockWatcher( "First Watcher" );
      var myStockWatcher2 = new NewStockWatcher( "Second Watcher" );
 
      myStockWatcher1.SetStockToWatch( myStock );
      myStockWatcher2.SetStockToWatch( myStock );
 
      while (1 == 1) {
        Console.WriteLine( "Please enter new stock price:" );
        string newStockPrice = Console.ReadLine();
        if (newStockPrice == String.Empty) break;
        if (Decimal.TryParse( newStockPrice, out myDecPrice ))
          myStock.Price = myDecPrice;
      }
    }
  }
}