Deep C# - Delegates
Written by Mike James   
Monday, 02 June 2025
Article Index
Deep C# - Delegates
The C# Approach
Delegate Patterns
Generic Delegates

Delegate Patterns

Why would you use a delegate rather than just calling a method?

The answer, explained in the introduction, is simply that a delegate can be passed as a parameter to another method, so determining what method is called at run time. More widely a delegate can be used anywhere an object can, which means you can have references to a delegate and data structures of delegates such as arrays. In functional programming terms, a delegate converts a method or a function into a "first-class object" i.e. one that can be used like any other object.

There are two well-known patterns that demand the use of a delegate. The first is the “callback” or notification method which is supplied to an object for it to call with intermediate or final results of its working. Of course, in this instance the object is usually run on a separate thread and the method provides some asynchronous communication between the caller and the called thread. The second is event handling. A delegate can be set up within an object so that clients can provide a method to be called when an event occurs. If you think carefully you will see that there is little difference between the callback and the event pattern. In both cases a delegate is called when some condition occurs – a buffer is full, the user has clicked a button, an error condition has been detected etc. However, while events are based on delegates they add an additional structure, an add and remove accessor similar to a property. A third, slightly less common, pattern involves creating a new thread of execution.

Signatures and Methods

At this point it is worth making clear that the delegate’s signature always determines how the method that the delegate wraps is called. That is, you always have to invoke the delegate with the parameter types specified and the delegate always returns the type specified. However, it is possible to create instances of the delegate that encapsulate methods that don’t have exactly the specified signature.

To summarize:

A delegate type’s signature specifies how the delegate is invoked – i.e. the parameters and return type are always given by the delegate’s signature.

A delegate type’s signature specifies what sort of methods an instance of the type can encapsulate.

Notice that the encapsulated method can be a static or an instance method. That is, the method can belong to an instance of the class or it can be a static "class method".

The delegate's Target property stores the instance and the Method property stores the method that that the delegate encapsulates. This is how a delegate "knows" what method to call. If the delegate encapsulates a static method then the Target is null, i.e. there is no instance. To encapsulate a method on a specific object you simply have to qualify the method name with the object’s name, for example this.hello or MyObject.hello.

csharp

Covariance and Contravariance Revisited

In the simplest case a method’s signature and return type have to match the signature of the delegate type that encapsulates it. However, there is more flexibility in how a method signature can match a delegate type signature. In the documentation this is called covariance and contravariance, just to make is sound more sophisticated. We have already met this idea twice before, in chapters 8 and 10, but it worth seeing how it works for delegates.

Put simply covariance allows the method to return a sub-class or derived type of the return type defined in the delegate. Assume for the moment that MyClassA is the base class and MyClassB is the derived class, that is:

public class MyClassA {};
public class MyClassB : MyClassA {};

If you now define the delegate type:

delegate MyClassA HelloType(MyClassB param);

then clearly a method returning MyClassA matches the signature. However, by covariance, so does any method returning a type that inherits from MyClassA, such as MyClassB. That is, you can use HelloType with a function like:

MyClassB myFunc1(MyClassB param);
HelloType myDelegate = new HelloType(myFunc1);

So the delegate type can encapsulate a method that returns a MyClassB. However, following the rule that the delegate signature determines its invocation, the return type is always treated as MyClassA. If the method does return a derived type, MyClassB say, you have to use a cast to work with the result as a MyClassB object, for example:

MyClassB myResult = (MyClassB) myDelegate(myB);

Contravariance allows the method to have parameters that are base types of the types specified in the delegate that encapsulates it. That is, if you define the delegate type as before:

delegate MyClassA HelloType(MyClassB param);

then a method that that has a parameter that is a base class for MyClassB, such as MyClassA, matches the signature. For example:

MyClassB myFunc2(MyClassA param);
HelloType myDelegate = new HelloType(myFunc2);

is perfectly acceptable. 

Once again, you can only invoke the delegate by passing a MyClassB object:

MyClassA myResult= myDelegate(myB);

but this all works because the method that the delegate invokes can treat this as a more primitive type, i.e. as a MyClassA object, which is what it wants to work with.



Last Updated ( Monday, 02 June 2025 )