Instruqt: Events

Nuts and Bolts

Events are the formalisation of the subscriber / publisher pattern we see when multicasting delegates. We have the invoker of the delegate, the “publisher”, and all the target methods which are executed, the “subscribers”.

We could stick with delegates to achieve the desired results, but the fact that subscribers can interfere with one another prevents this from being a great solution. Subscribers could reset the delegate by setting it to null, thereby removing all subscribers, and they could remove specific subscribers by using the -= operator.

Events exist to prevent subscribers from interfering with one another. This is achieved by wrapping delegate functionality to make available only the subset of functionality required to support the publisher / subscriber model.

Events are declared as follows:

public class Broadcaster {
    public event NotifyThingy Notifyer; 
}

In the above declaration, NotifyThingy would be a delegate, declared within scope as follows:

public delegate void NotifyThingy(string message);

So it’s clear from the example above that delegates are driving the event implementation, which makes sense as events are just a wrapper as mentioned before.

The Standard Event Pattern

A standard pattern exists for event implementations which completes the formalisation of the pub/sub model. The pattern exists of three parts:

  1. The event declaration
  2. An EventArgs implementation for conveying information within the event
  3. A function to wrap event invocation by the publisher

Here is an example:

public class Stock {
    public EventHandler<PriceChangedEventArgs> PriceChanged;

    protected virtual void OnPriceChanged(int oldPrice, int newPrice) {
      if (PriceChanged != null)
        PriceChanged(this, new PriceChangedEventArgs(oldPrice, newPrice, Symbol));
    }

    public readonly string Symbol;

    public Stock(string symbol) {
      Symbol = symbol;
    }

    public int Price {
      get { return _price; }
      set {
        if (_price != value)
          OnPriceChanged(_price, value);

        _price = value;
      }
    }
}

public class PriceChangedEventArgs : EventArgs {
  public readonly int OldPrice;
  public readonly int NewPrice;
  public readonly string Symbol;

  public PriceChangedEventArgs(int oldPrice, int newPrice, string symbol) {
    OldPrice = oldPrice;
    NewPrice = newPrice;
    Symbol = symbol;
  }
}

A few things to take notice of:

  1. The generic EventHandler<T> delegate
  2. The fact that the invocation wrapper function is protected virtual, meaning subclasses can fire the event, and execute code before and after the event.
  3. The null check in the event invocation wrapper function

The EventHandler<T> delegate means we can forgo the need to declare an underlying delegate explicitly for our events. A non-generic EventHander delegate also exists for when we don’t need to pass anything in the event.

 

Instruqt: Delegates

Nuts and bolts

A Delegate is a type which represents a function pointer, and as such it enables you to dynamically wire up a method caller to the target method. It literally delegates a method call from the delegate to the target method(s).

A Delegate Type defines the delegate protocol, or method signature of the target methods:

delegate int NumberCruncher(int a, int b);

a Delegate Instance represents a method which matches the protocol and has been associated with the delegate:

NumberCruncher multiplier = MultiplyNumbers;

private int MultiplyNumbers(int a, int b) {
    return a*b;
}

All delegates have multicast capabilities (as they implicitly inherit from System.MulticastDelegate), meaning that multiple target methods can be associated to a single delegate, or more correctly, multiple methods can be associated to a delegate instance. This is achieved by using the += operator:

multiplier += AddNumbers;

private int AddNumbers(int a, int b) {
    return a+b;
}

Invoking the delegate will result in all the associated methods executing in the order in which they were added. If the delegate has a return value, only the return value of the last method will the returned, with all other return values discarded.

Removing a method from the list of target methods is done by using the -= operator. The += and -= operators are compiled to the static Combine and Remove methods of the Delegate class.

Plugin Methods

Delegates are handy for plugging dynamic functionality into another method:

public void PerformANumericDuty(int[] values, NumberCruncher cruncher) {
    //do something here...    

    for(int i = 0; i < values.Length; i++)
        values[i] = cruncher(values[i], 10)
    }
}

Generic Delegates

Delegates may use generic type parameters:

delegate T GenericDelegate<T>(T arg);

GenericDelegate<double> squarer = Square;

private double Square(double a){
    return a*a;
}

Contravariance

Delegates are contravariant, meaning that the delegate type parameters can be more specific than the delegate instance type parameters:

class House : Asset { ... }

delegate void SellHouse(House house);

...

private void SellAsset(Asset asset){ ... }

...

SellHouse houseSeller = SellAsset;

...

houseSeller(new House());

Covariance

Delegates are covariant, meaning that the return type of a delegate type can be less specific than the return type of the delegate instance.

class House : Asset { ... }

delegate Asset GetAsset();

...

GetAsset getMyHouse = GetHouse();

...

private House GetHouse() { ... }

...

Asset myhouse = getMyHouse();