9) Delegates
9) Delegates
// A callback mechanism refers to a technique where a function (referred to as the callback function) is passed as an argument to another function ( often
referred to as the caller function) and is executed after the completion of a particular task or event. The purpose of a callback is to allow code to be
executed in response to certain events or at a specific point during the execution of a program.
// Once the main task is complete, the caller function invokes (or "calls back") the callback function that was passed to it.
// Asynchronous Operations: Callbacks are often used in asynchronous operations (e.g., file reading, network requests) where the main task continues
running while waiting for another task to complete. Operations like network requests, file operations, or database queries can take time to complete.
During that time, you don't want the entire program to stop and wait. A callback allows the program to continue running other code and only execute the
callback function once the task is done.
// Event-Driven Programming: Callbacks are frequently used in event-driven programming, such as in GUIs or web development. Callbacks are used to
handle unpredictable events like button clicks, key presses, or incoming messages. These events happen at unknown times, and you can't just place a
method at the end of the code or use if-else, as you don't know when the event will occur.
// Concurrency: In more advanced applications, callbacks allow you to handle multiple tasks that run concurrently. Without callbacks (or similar
mechanisms), you'd need to block the execution of other tasks while one is being completed, which reduces the performance of the application.
// In C , using pointer function callbacks, programmers were able to configure one 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 two entities in memory.
// In C#, a callback can be implemented using delegates, lambda expressions, or events.
// A delegate is a type-safe object/data structure that points to another method (or possibly a list of methods) in the application, which can be invoked
later. Delegate can be said to be a class type object which is used to invoke a method that has been encapsulated into it at the time of its creation. After a
delegate object has been created and given the necessary information, it may dynamically invoke the method(s) it points to at runtime (pointer/reference
to method). Specifically, a delegate maintains three important pieces of information.
The address of the method on which it makes calls , The parameters (if any) of this method & The return type (if any) of this method.
// Following are the steps required in using delegate:
1. Define/Declare Delegate: Start with defining a delegate (type declaration) . Following declaration uses keyword delegate – represents a class derived
from System.Delegate as a base class. We must supply the signature (return type and parameters) of the methods it can point to – signature matching is
must for using delegates. We must define the delegate so as to match the signature of the method(s) it will point to.
Syntax: modifier delegate ReturnType DelegateName(ParameterList);
When the 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 infrastructure for the delegate to hold onto a list of methods to be invoked
later(can be seen running ildassm.exe on delegate declaration). The Compiler-generated Delegate class defines some public methods. One of them -
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. Invoke() is called behind the scenes when you use the appropriate C# syntax – parameter and return types of Invoke() are based on the delegate’s
declaration.
When you build a type using the C# delegate keyword, you are indirectly declaring a class type that derives from System.MulticastDelegate. This
class provides descendants with access to a list
that contains the addresses of the methods
maintained by the delegate object, as well as
several additional methods (and a few
overloaded operators) to interact with the
invocation list. 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, which in turn
derives additional functionality from its parent
class, System.Delegate.
Some core members common to all delegate
types are given alongside:
Also, we cannot declare a delegate inside of Main() because it's a type definition, not a variable declaration. Any type definition is not allowed inside
method. It's allowed only at class scope, or namespace scope. This would also limit the scope of the delegate – it wouldn't be able communicable to other
methods.
Why can I not declare a type inside a method in C#? – No one has made a compelling case that local type declarations are worthwhile, hence this
feature is as of yet not implemented.
2. Create Methods: Implement methods (can be static or instance methods) that match the delegate's signature (return type and parameter list). These
are methods whose references will be later encapsulated into a delegate instance.
3. Instantiate Delegate:The delegate instance will hold the reference to the delegate methods. Create an instance of the delegate and assign it to the target
method (or methods) by simply passing the name of the method to the delegate’s constructor. The method(s) must match the delegate signature.
Syntax: DelegateName delInstance = new DelegateName(expression);
// The expression could be a method name or a value of a delegate type. The method and the object to which a delegate refers are determined when the
delegate is instantiated and then remains constant for the entire lifetime of the delegate. It is therefore not possible to change them, once the delegate is
created. It is also not possible to create a delegate that would refer to a constructor, indexer, or user-defined operator.
//Note : DelegateName delInstance; //Simply writing this won’t create an instance, delInstance is simply a variable that can hold a reference to a method
matching the delegate's signature, but it doesn't reference any method yet (it's null by default). The instance of the delegate is created when you assign a
method to it using new keyword or simpy using = to reference the Static or Instance method. In modern C# version, the C# compiler automatically infers
that you are creating a delegate instance based on the method’s signature. This is called delegate inference, which simplifies the syntax and makes the code
cleaner and easier to read. NET delegates are type-safe. Therefore, if you attempt to create a delegate object pointing to a method that does not match the
pattern, you receive a compile-time error.
4. Invoke Delegate: Call the delegate like a method. Invoke the member pointed to using a syntax that looks like a direct function invocation.This will
invoke the method(s) it points to.
Syntax: delInstance(arguments); // In this line, the Invoke() method is really called here behind the scenes – can be seen using ildasm.exe
C# does not require you to explicitly call Invoke() within your code base.
namespace Basic
{
internal class Program
{
public class SimpleMath
{
public static int Add(int x, int y) => x + y; //This is step 2 – creating delegate methods
public static int Subtract(int x, int y) => x - y; //These methods match the signature of delegate in step 1
public int Multiply(int x, int y) => x * y; //We could have non static methods as well
}
static void DelegateInfo(Delegate delObj) //We can Investigate a Delegate Object by calling the members of…
{ //Here Delegate is System.Delegate // …MultiCast Delegate on the delegate object
foreach(Delegate d in delObj.GetInvocationList())
{ //Iterate over the System.Delegate array returned by GetInvocationList()
Console.WriteLine("Method Name: {0}", d.Method); //Print names of methods maintained by delegate object
Console.WriteLine("Type Name: {0}", d.Target); //Print name of the class defining the method(target class)
} //Note : No target class is displayed using Target property if delegate is pointing to static method- since there is no object to reference!
}
public delegate int BinaryOp(int x, int y); //This is step 1- declaring a delegate using the keyword
// Note the signature: return type int, 2 int parameters
static void Main(string[] args)
{
Console.WriteLine("*******Simple Delegate Example*************");
BinaryOp adder = new BinaryOp(SimpleMath.Add); //Step 3 - BinaryOp delegate object points to SimpleMath.Add
BinaryOp subtractor = new BinaryOp(SimpleMath.Subtract); // class name is directly used since method is static
Console.WriteLine("5 + 10 is {0}", adder(5,10)); // Step 4-Invoke Add() method indirectly using delegate object.
Console.WriteLine("5 - 10 is {0}", subtractor(5,10));
SimpleMath m = new SimpleMath(); // For non static method, we create an object of the class
BinaryOp productor = new BinaryOp(m.Multiply);
DelegateInfo(productor); // Returns ->
Console.ReadLine();
}
}
}
There would be no compelling reason to define a delegate simply to add two numbers. Or if we use delegates to call methods indirectly, it wouldn't add
much value. One of the main uses of Delegates is to act as Event Enablers, not just calling another method. The true power of delegates lies in scenarios
where you need flexibility, extensibility, and dynamic behavior, especially when working with event-driven programming, callbacks, or designing reusable
libraries. To provide a more realistic use of delegate types, let’s use delegates to define a Car class that can inform external entities (Sending notifications
through different channels) about its current engine state.
namespace CarDelegate // This is a separate C# file for defining Car class
{ //Reference this project in the main startup project shown later below
public class Car
{
public string PetName { get; set; } // Internal state data
public int CurrentSpeed { get; set; }
public int MaxSpeed { get; set; } = 100;
private bool IsCarDead;
public Car() { } // Two Class constructors
public Car(string name, int maxSp, int currSp)
{
PetName = name;
CurrentSpeed = currSp;
MaxSpeed = maxSp;
} // Next, we define the delegate types directly within the scope of the Car class
public delegate void CarEngineHandler(string msgForCaller); // Define a Delegate type
// delegate CarEngineHandler, can point to any method taking a single string as input and void as a return value.
private CarEngineHandler _listOfHandlers; // Define a member variable of this delegate type
//We could have defined delegate member variable as public, thus avoiding the need to create additional registration methods.
// However, by defining the delegate member variable as private, we enforce encapsulation services & provide a more type-safe solution
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
{ // This Helper function ,RegisterWithCarEngine(), allows the caller to assign a method to the delegate’s invocation list
_listOfHandlers += methodToCall; // .NET delegates have built-in ability to multicast
// In other words, a delegate object can maintain a list of methods to call, rather than just a single method
} // To add multiple methods to a delegate object, you simply use the overloaded += operator, rather than a direct assignment
public void Accelerate(int delta) // This method invokes the delegate's invocation list under the correct circumstances
{ //We use the null propagation syntax when attempting to invoke the methods maintained by the listOfHandlers member variable
if (IsCarDead)
{ //It acts as a safeguard against trying to call a method on a null reference
_listOfHandlers?.Invoke("Sorry, this car is dead...."); // If _listOfHandlers is null…
} //..the expression will evaluate to null and the Invoke method will not be called. This helps avoid a NullReferenceException.
CurrentSpeed += delta;
if((MaxSpeed-CurrentSpeed)== 10)
{
_listOfHandlers?.Invoke("Careful Buddy! Gonna Blow....");
}
if(CurrentSpeed >= MaxSpeed)
{
IsCarDead = true;
}
else
{
Console.WriteLine("Current Speed: {0}", CurrentSpeed);
}
}
}
}
// ?. notation is called the null-conditional operator in C#. It is used to perform a member access (like invoking a method, accessing a property, or indexing)
only if the left-hand side is not null.
using CarDelegate;
namespace CarMakeandRegister
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("*********Delegates as Events Enablers************");
Car c1 = new Car("SlugBug", 100, 10);
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent2));
Console.WriteLine("***** Speeding up *****");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();
}
static void OnCarEngineEvent(string msg)
{
Console.WriteLine("\n*** Message From Car Object ***");
Console.WriteLine("=> {0}", msg);
Console.WriteLine("********************\n");
}
static void OnCarEngineEvent2(string msg)
{
Console.WriteLine("=> {0} ", msg.ToUpper());
}
}
}