The delegate type is the preferred means of defining and responding to callbacks within applications. The .NET delegate type is a type-safe object that "points to" a method or a list of methods that can be invoked later.


The C# "event" keyword, which streamlines the process of working with delegate types.


Using C# lambda operator (=>) you can specify a block of code statements (and the parameters to pass to those code statements) wherever a strongly typed delegate is required.



Understanding the Delegate Type


The Windows API mode frequent use of C-Style function pointers to create entities termed callback functions, or simply callbacks. Using callbacks, programmers were able to configure are function to report back to (call back) another function in the application. With this approach, Windows developers were able to handle button clicking, mouse moving, menu selecting, and general bidirectional communications between 2 entities in memory.


In the .NET and .NET Core Frameworks, callbacks are accomplished in a type-safe and object-oriented manner using delegates. A delegate is a type-safe object that points to another method (or possibly a list of methods) in the application, which can invoked later. Specifically, a delegate maintains 3 important pieces of information.

  1. The address of the method on which it makes calls.
  2. The parameters (if any) of this method
  3. The return type (if any) of this method



Note: .NET delegates can point to either static or instance methods.


After a delegate object has been created and given the necessary information, it may dynamically invoke the method(s) it points to at runtime.


Defining a Delegate Type in C#

When you want to create a delegate type in C#, you use the "delegate" keyword.


// This delegate can point to any method, taking 2 integers and returning an integer
public delegate int BinaryOp(int x, int y);


When type C# compiler processes delegate types, it automatically generates a sealed class deriving from System.MulticastDelegate. This class (in conjunction with its base class System.Delegate) provides the necessary information for the delegate to hold onto a list of methods to be invoked later.


The compiler-generated BinaryOp class defines 3 public methods. Invoke() is the key method in .NET, as it is used to invoke each method maintained by the delegate object in a synchronous manner, meaning the caller must wait for the call to complete before continuing its way. Strangely enough, the synchronous Invoke() method may not need to be called explicitly from your C# code.


Now, how exactly does the compiler know how to define the Invoke() method ?


internal sealed class BinaryOp : MulticastDelegate
 {
     public int Invoke(int x, int y);
     ...
 }
 
public delegate string MyDelegate(bool a, bool b, bool c);
 
sealed class MyDelegate : System.MulticastDelegate
 {
     public string Invoke(bool a, bool b, bool c);
 }


Delegates can also "point to" methods that contain any number if "out" or "ref" parameters (as well as array parameters marked with-the params keyword).


public delegate string MyOtherDelegate(out bool a, ref bool b, int c);


To summarize, a C# delegate type definition results in a sealed class with a compiler-generated method whose parameter and return types are based on the delegates declaration. The following pseudocode approximates the basic pattern:


public sealed class DelegateName : System.MulticastDelegate
 {
     public delegateReturnValue Invoke(allDelegateInputRefAndOutParameters);
 }


The System.MulticastDelegate and System.Delegate Base Class

So, when you build a type using the C# delegate keyword, you are indirectly declaring a class type that derives from System.MulticastDelegate.


System.MulticastDelegate obtains additional functionality from its parent class, System.Delegate.


Understand that you can never directly derive from these base classes in your code (it is a compiler error to do so). Nevertheless, when you use the "delegate" keyword, you have indirectly created a class that "is-a" MulticastDelegate.


public abstract class MulticastDelegate : Delegate
 {
     // Returns the list of methods "pointed to."
     public sealed override Delegate[] GetInvocationList();
     // Overloaded operators.
     public static bool operator == (MulticastDelegate d1, MulticastDelegate d2);
     public static bool operator != (MulticastDelegate d1, MulticastDelegate d2);
     // Used internally to manage the list of methods maintained by the delegate.
     private IntPtr _invocationCount;
     private object _invocationList;
 }
 
public abstract class Delegate : ICloneable, ISerializable
 {
     // Methods to interact with the list of functions.
     public static Delegate Combine(params Delegate[] delegates);
     public static Delegate Combine(Delegate a, Delegate b);
     public static Delegate Remove(
     Delegate source, Delegate value);
     public static Delegate RemoveAll(
     Delegate source, Delegate value);
     // Overloaded operators.
     public static bool operator ==(Delegate d1, Delegate d2);
     public static bool operator !=(Delegate d1, Delegate d2);
     // Properties that expose the delegate target.
     public MethodInfo Method { get; }
     public object Target { get; }
 }


  1. Method => This property returns a, System.Reflection.MethodInfo object that represents details of a static method maintained by the delegate.
  2. Target => If the method to be called is defined at the object level (rather tan a static method). Target returns an object that returns the method maintained by the delegate. If the value returned from Target equals null, the method to be called is a static member.
  3. Combine() : This static method adds a method to the list maintained by the delegate. In C#, you trigger this method using the overloaded += operator as a shorthand notation.
  4. GetInvocationList() : This method returns an array of System.Delegate objects, each representing a method that may be invoked.
  5. Remove() / RemoveAll() : These static methods remove a method (or all methods) from the delegates invocation list. In C#, the Remove() method can be called indirectly using the overloaded -= operator.


The Simplest Possible Delegate Example


public class SimpleMath
 {
     public static int Add(int x, int y) => x + y;
     public static int Subtract(int x, int y) => x - y;
 }


BinaryOp b = new BinaryOp(SimpleMath.Add);
// Invoke Add() method indirectly using delegate object.
Console.WriteLine(b(10,10));


Under the hood, the runtime calls the compiler-generated Invoke method on your MulticastDelegate-derived class.


// Additional type definitions must be placed at the end of the top-level statements
// This delegate can point to any method, taking 2 integers and returning an integer.
public delegate int BinaryOp(int x, int y);


C# does not require you to explicitly call Invoke() within your code base. Because BinaryOp can point to methods that take 2 arguments.


Console.WriteLine(b.Invoke(10, 10)); // Permissible
 
Investigating a Delegate Object
static void DisplayDelegateInfo(Delegate delObj)
 {
     foreach(Delegate d in delObj.GetInvocationList())
     {
         Console.WriteLine("Method Name : "+d.Method); // Int32 Add(In32,In32)
         Console.WriteLine("Type Name : "+d.Target);
     }
 }
 
BinaryOp b = new BinaryOp(SimpleMath.Add);
DisplayDelegateInfo(b);


Notice that the name of the target class (SimpleMath) is not currently not displayed when calling the Target property. The reason has to do with the fact that your BinarOp delegate is pointing to a static method and, therefore, there is no object to reference. Update methods (Add and Subtract) static -> non-static


SimpleMath m = new SimpleMath();
BinaryOp b = new BinaryOp(m.Add);
DisplayDelegateInfo(b); // SimpleDelegate.SimpleMath



Sending Object State Notifications Using Delegates


  1. Define a new delegate type that will be used to send notifications to the caller.
  2. Declare a member variable of this delegate in the Car class.
  3. Create a helper function on the Car class that allows the caller to specify the method to call back on.
  4. Implement the Accelerate() method to invoke the delegate's invocation list under the correct circumstances.


class Car
 {
     public int CurrentSpeed { get; set; }
     public int MaxSpeed { get; set; }
     public string PetName { get; set; }
     private bool _carIsDead;
     public Car()
     {
             
     }
     public Car(string name, int speed, int maxSp)
     {
         CurrentSpeed = speed;
         PetName = name;
         MaxSpeed = maxSp;
     }
 }
 
// Define a delegate type in Car Class
public delegate void CarEngineHandler(string msgForCaller);
 
 
// Define a mamber variable of this delegate in Car class
private CarEngineHandler _listOfHandlers;
// Add registration function for the caller in Car Class
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
 {
     _listOfHandlers = methodToCall;
 }
// Implement the Accelerate() method
public void Accelerate(int delta)
 {
     if (_carIsDead)
     {
         _listOfHandlers.Invoke("Sorry, this car is dead");
     }else
     {
         CurrentSpeed += delta;
         // Almost dead!
         if (10 == (MaxSpeed - CurrentSpeed))
         {
             _listOfHandlers.Invoke("Careful");
         } 
         if (CurrentSpeed >= MaxSpeed)
         {
             _carIsDead = true;
         }else
         {
             Console.WriteLine($"Current speed {CurrentSpeed}");
         }
     }
 }
 
 
static void Main(string[] args)
 {
     Car c1 = new Car("SlugBug", 10, 100);
     c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineHandler));
   for(int i = 0; i < 6; i++)
     {
         c1.Accelerate(20);
     }
 }
private static void OnCarEngineHandler(string msgForCaller)
 {
     Console.WriteLine("Message for object : "+msgForCaller);
 }


Enabling Multicasting

A delegate object can maintain a list of methods to call, rather than just a single method. When you want to add multiple methods to a delegate object you simply use the overloaded += operator, rather than a direct assignment.

class Car
{
....
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
 {
     _listOfHandlers += methodToCall;
 }
}


When you use the += operator on a delegate object, the compiler resolves this to a call on the static Delegate.Combine() method. In fact, you could call Delegate.Combine() directly; however, the += operator offers a simpler alternative.


public void RegisterWithCarEngine(CarEngineHandler methodToCall)
 {
     if (_listOfHandlers == null)
     {
         _listOfHandlers = methodToCall;
     }else
     {
         _listOfHandlers = Delegate.Combine(_listOfHandlers,methodToCall) as CarEngineHandler;
     }
 }
 
Car c1 = new Car("SlugBug", 10, 100);
// Register multiple targets for the notifications.
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineHandler));
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineHandler2));
for(int i = 0; i < 6; i++)
 {
     c1.Accelerate(20);
 }
 
private static void OnCarEngineHandler(string msgForCaller)
 {
     Console.WriteLine("Message for object : "+msgForCaller);
 }
private static void OnCarEngineHandler2(string msgForCaller)
 {
     Console.WriteLine(msgForCaller.ToUpper());
 }


Removing Targets from a Delegate's Invocation List

The Delegate class also defines a static Remove() method that allows a caller to dynamically remove a method from a delegate object's invocation list. This makes it simple to allow the caller to "unsubscribe" from a given notification at runtime. While you could call Delegate.Remove() directly in code, C# developers can use the -= operator as a convenient shorthand notation.

class Car
 {
...
public void UnregisterWithCarEngine(CarEngineHandler methodToCall)
 {
     _listOfHandlers -= methodToCall;
 }
}
 
Car c1 = new Car("SlugBug", 10, 100);
// Register multiple targets for the notifications.
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineHandler));
// This time, hold onto the delegate object so we can unregister later.
Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OnCarEngineHandler2);
c1.RegisterWithCarEngine(handler2);
for(int i = 0; i < 6; i++)
 {
     c1.Accelerate(20);
 }
// Unregister from the second handler
c1.UnregisterWithCarEngine(handler2);
// We won't see the "uppercase" message anymore!
for(int i = 0; i < 6; i++)
 {
     c1.Accelerate(20);
 }


Method Group Conversion Syntax

C# provides a shortcut termed method group conversion. This feature allows you to supply a direct method name, rather than a delegate object, when calling methods that take delegates as arguments.


Car c2 = new Car();
// Register the simple method name
c2.RegisterWithCarEngine(OnCarEngineHandler);
for(int i = 0; i < 6; i++){c2.Accelerate(20);}
// Unregister the simple method name
c2.UnregisterWithCarEngine(OnCarEngineHandler);
for(int i = 0; i < 6; i++){c2.Accelerate(20);}


Notice that you are not directly allocating the associated delegate object but rather simply specifying a method that matches the delegate's expected signature (a method returning void and taking a single string, in this case)