Delegate instances are considered equal if they have the same method targets:
delegate void D(); ...
D d1 = Method1; D d2 = Method1; Console.WriteLine (d1 == d2); // true
Parameter compatibility
When you call a method, you can supply arguments that have more specific types than the parameters of that method. This is ordinary polymorphic behavior. For exactly the same reason, a delegate can have more specific parameter types than its method target. This is called contravariance.
Consider the following example:
delegate void SpecificDelegate (SpecificClass s);
class SpecificClass {}
class Test { static void Main() { SpecificDelegate specificDelegate = GeneralHandler; specificDelegate (new SpecificClass()); }
A delegate merely calls a method on someone else’s behalf. In this case, theSpecificDelegateis invoked with an argument of typeSpecificClass. When the argument is then relayed to the target method, the argument gets implicitly upcast to anobject.
The standard event pattern is designed to help you leverage contravariance through its use of the commonEventArgsbase class. For example, you can have a single method invoked by two different delegates, one passing aMouseEventArgsand the other passing aKeyEventArgs.
Return type compatibility
If you call a method, you may get back a type that is more specific than what you asked for. This is ordinary polymorphic behavior. For exactly the same reason, the return type of a delegate can be less specific than the return type of its target method. This is called covariance. Consider the following example:
delegate Asset DebtCollector();
class Asset {}
class House : Asset {}
class Test { static void Main() { DebtCollector d = new DebtCollector (GetHomeSweetHome); Asset a = d(); Console.WriteLine(a.GetType()); // House } static House GetHomeSweetHome() {return new House(); } }
A delegate merely calls a method on someone else’s behalf. In this case, theDebtCollectorexpects to get back anAsset—but anyAssetwill do. Delegate return types are said to be covariant.
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. Aneventis 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.
To declare an event member, you put theevent keyword in front of a delegate member. For instance:
public class Broadcaster { public event ProgressReporter Progress; }
Code within theBroadcastertype has full access toProgressand can treat it as a delegate. Code outside ofBroadcastercan only perform+=and-=operations onProgress.
Consider the following example. TheStockclass invokes itsPriceChangedevent every time thePriceof theStock changes:
public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice);
public class Stock { string symbol; decimal price;
public Stock (string symbol) {this.symbol = symbol;}
public event PriceChanged PriceChanged;
public decimal Price { get { return price; } set { if (price == value) return; // exit if nothing has changed if (PriceChanged != null) // if invocation list not empty PriceChanged (price, value); // fire event price = value; } } }
If we remove theeventkeyword from our example so thatPriceChangedbecomes an ordinary delegate field, our example would give the same results. However,Stockwould be less robust, in that subscribers could do the following things to interfere with each other:
Replace other subscribers by reassigningPriceChanged(instead of using the+=operator).
Clear all subscribers (by settingPriceChangedtonull).
Broadcast to other subscribers by invoking the delegate.
The .NET Framework defines a standard pattern for writing events. Its purpose is to provide consistency across both Framework and user code. At the core of the standard event pattern is System.EventArgs: a predefined Framework class with no members (other than the static Empty property).EventArgsis a base class for conveying information for an event. In ourStockexample, we would subclassEventArgsto convey the old and new prices when aPriceChangedevent is fired:
public class PriceChangedEventArgs : System.EventArgs { public readonly decimal LastPrice; public readonly decimal NewPrice;
For reusability, theEventArgssubclass is named according to the information it contains (rather than the event for which it will be used). It typically exposes data as properties or as read-only fields.
With anEventArgssubclass in place, the next step is to choose or define a delegate for the event. There are three rules:
It must have avoidreturn type.
It must accept two arguments: the first of typeobject, and the second a subclass ofEventArgs. The first argument indicates the event broadcaster, and the second argument contains the extra information to convey.
Its name must end in “EventHandler”.
The Framework defines a generic delegate calledSystem.EventHandler<>that satisfies these rules:
public delegate void EventHandler<TEventArgs> (object source, TEventArgs e) where TEventArgs : EventArgs;
Before generics existed in the language (prior to C# 2.0), we would have had to instead write a custom delegate as follows:
public delegate void PriceChangedHandler (object sender, PriceChangedEventArgs e);
For historical reasons, most events within the Framework use delegates defined in this way.
The next step is to define an event of the chosen delegate type. Here, we use the genericEventHandler delegate:
public class Stock { ...
public event EventHandler<PriceChangedEventArgs>PriceChanged; }
Finally, the pattern requires that you write a protected virtual method that fires the event. The name must match the name of the event, prefixed with the word “On”, and then accept a singleEventArgsargument:
public class Stock { ...
public event EventHandler<PriceChangedEventArgs> PriceChanged;
The predefined nongenericEventHandler delegate can be used when an event doesn’t carry extra information. In this example, we rewriteStocksuch that thePriceChangedevent is fired after the price changes, and no information about the event is necessary, other than it happened. We also make use of theEventArgs.Emptyproperty, in order to avoid unnecessarily instantiating an instance ofEventArgs.
public class Stock { string symbol; decimal price;
public Stock (string symbol) {this.symbol = symbol;}
An event’s accessors are the implementations of its += and -= functions. By default, accessors are implemented implicitly by the compiler. Consider this event declaration:
public event EventHandler PriceChanged;
The compiler converts this to the following:
A private delegate field
A public pair of event accessor functions, whose implementations forward the+=and-=operations to the private delegate field
You can take over this process by defining explicit event accessors. Here’s a manual implementation of thePriceChangedevent from our previous example:
private EventHandler _PriceChanged; // declare a private delegate
This example is functionally identical to C#'s default accessor implementation. The add and remove keywords after the event declaration instruct C# not to generate a default field and accessor logic.
With explicit event accessors, you can apply more complex strategies to the storage and access of the underlying delegate. There are three scenarios where this is useful:
When the event accessors are merely relays for another class that is broadcasting the event.
When the class exposes a large number of events, where most of the time very few subscribers exist, such as a Windows control. In such cases, it is better to store the subscriber’s delegate instances in a dictionary, since a dictionary will contain less storage overhead than dozens of null delegate field references.
When explicitly implementing an interface that declares an event.
Here is an example that illustrates the last point:
public interface IFoo { event EventHandler Ev; }
class Foo : IFoo { private EventHandler ev;
event EventHandler IFoo.Ev { add { ev += value; } remove { ev -= value; } } }
Theaddandremove parts of an event are compiled toadd_XXX andremove_XXX methods.
The+=and-=operations on an event are compiled to calls to theadd_XXX andremove_XXX methods.
Event Modifiers
Like methods, events can be virtual, overridden, abstract, and sealed. Events can also be static:
public class Foo { public static event EventHandler<EventArgs>StaticEvent; public virtual event EventHandler<EventArgs>VirtualEvent; }
Please check back next week for the continuation of this article.