Delegates, acting as type-safe function pointers, provide the foundation for dynamic method invocation, and treat methods as first-class citizens, passing them as arguments and storing them in variables.

Lambdas, concise anonymous functions, offer a streamlined way to define these methods inline, often simplifying delegate creation.

Events, built upon the powerful mechanism of multicast delegates, enable the elegant implementation of the observer pattern.

1. Delegates

Delegates are type-safe, secure, and verifiable reference types in .NET, similar in purpose to function pointers in C++, and commonly used for event handlers and callback functions. [1]

  • A delegate type can represent any instance method or static method that has a compatible signature.

  • A delegate’s parameter is compatible with the corresponding parameter of a method if the type of the delegate’s parameter is more restrictive than the type of the method parameter, because an argument passed to the delegate can be passed safely to the method.

    DogHandler dogHandler = AnimalMethod; // allowed!
    AnimalHandler animalHandler = DogMethod; // compiler error!
    
    static void AnimalMethod(Animal a) => Console.WriteLine("Animal method called");
    static void DogMethod(Dog d) => Console.WriteLine("Dog method called");
    
    delegate void DogHandler(Dog d);
    delegate void AnimalHandler(Animal a);
    
    class Animal { }
    class Dog : Animal { }
  • A delegate’s return type is compatible with the return type of a method if the return type of the method is more restrictive than the return type of the delegate, because the return value of the method can be cast safely to the return type of the delegate.

    AnimalGetter animalGetter = () => new Dog();
    Animal myAnimal = animalGetter(); // No problem!
    
    static Dog GetDog() => new();
    
    delegate Animal AnimalGetter();

    Liskov’s notion of a behavioural subtype defines a notion of substitutability for objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g. correctness).

1.1. Delegate and MulticastDelegate

All delegates inherit from System.MulticastDelegate, which inherits from System.Delegate.

public abstract class MulticastDelegate : Delegate
  • The C#, Visual Basic, and C++ languages do not allow inheritance from these types.

  • Instead, they provide keywords for declaring delegates.

  • A delegate, inherit from MulticastDelegate, has an invocation list, which is a list of methods that the delegate represents and that are executed when the delegate is invoked.

    • All methods in the list receive the arguments supplied when the delegate is invoked.

    • The return value is not defined for a delegate that has more than one method in its invocation list, even if the delegate has a return type.

      // Create a delegate instance using a lambda expression.
      MyDelegate myDelegate = () => Console.Write("Hello, ");
      
      // Combine (add) another lambda expression to the delegate, making it multicast.
      myDelegate += () => Console.WriteLine("World!");
      
      // Invoke the delegate; both lambda expressions will be executed.
      myDelegate(); // Output: Hello, World!
      
      // Declare a delegate type for methods with no parameters and void return.
      public delegate void MyDelegate();
  • In many cases, such as with callback methods, a delegate represents only one method, and the only actions you have to take are creating the delegate and invoking it.

  • A delegate object is normally constructed by providing the name of the method the delegate will wrap, or with a lambda expression.

    public delegate void Callback(string message);
    
    // Create a method for a delegate.
    public static void DelegateMethod(string message) => Console.Write(message);
    
    Callback handler1 = DelegateMethod; // method name
    Callback handler2 = s => Console.WriteLine(s); // lambda expression
    
    // Call the delegate.
    handler1("Hello, ");
    handler2("World!");
    // Hello, World!
  • A delegate can call more than one method when invoked, which is referred to as multicasting.

    • For delegates that represent multiple methods, .NET provides methods of the Delegate and MulticastDelegate delegate classes to support operations such as:

      • the Delegate.Combine method to add a method to a delegate’s invocation list.

      • the Delegate.Remove method to remove a method, and

      • the Delegate.GetInvocationList method to get the invocation list.

    • To add an extra method to the delegate’s list of methods—the invocation list—simply requires adding two delegates using the addition or addition assignment operators (+ or +=).

      var obj = new MethodClass();
      Callback d1 = obj.Method1;
      Callback d2 = obj.Method2;
      Callback d3 = MethodClass.Method3;
      
      Callback allMethodsDelegate = d1 + d2;
      allMethodsDelegate += d3;
      allMethodsDelegate -= d2;
      Delegate[] delegates = allMethodsDelegate.GetInvocationList();
      int invocationCount = delegates.Length;
      
      public class MethodClass
      {
          public void Method1(string message) => Console.WriteLine($"Method 1: {message}");
          public void Method2(string message) => Console.WriteLine($"Method 2: {message}");
          public static void Method3(string message) => Console.WriteLine($"Method 3: {message}");
      }

1.2. Func<>, Action<>, and Predicate<>

In order to streamline the development process, .NET includes a set of delegate types that programmers can reuse and not have to create new types, which are Func<>, Action<> and Predicate<>.

  • Action<> - Represents a method that takes up to 16 input parameters and has a void return type.

    // Action with no parameters
    Action myAction1 = () => Console.WriteLine("Action with no parameters");
    myAction1();
    
    // Action with one parameter
    Action<int> myAction2 = x => Console.WriteLine($"Action with one parameter: {x}");
    myAction2(10);
    
    // Action with two parameters
    Action<int, string> myAction3 = (x, s) => Console.WriteLine($"Action with two parameters: {x}, {s}");
    myAction3(20, "Hello");
  • Func<> - Represents a method that takes up to 16 input parameters and returns a value of the type specified by the TResult parameter, and last type parameter is always the return type.

    // Func with no parameters (returns a string)
    Func<string> myFunction1 = () => "Func with no parameters";
    string result1 = myFunction1();
    Console.WriteLine(result1);
    
    // Func with one parameter (takes an int, returns a string)
    Func<int, string> myFunction2 = x => $"Func with one parameter: {x}";
    string result2 = myFunction2(30);
    Console.WriteLine(result2);
    
    // Func with two parameters (takes int and string, returns bool)
    Func<int, string, bool> myFunction3 = (x, s) => x > 0 && !string.IsNullOrEmpty(s);
    bool result3 = myFunction3(40, "Test");
    Console.WriteLine(result3);
  • Predicate<> - Represents a method, a specialized version of Func, that takes a single input parameter and returns a boolean value.

    Predicate<int> myPredicate1 = x => x > 50;
    bool result4 = myPredicate1(60);
    Console.WriteLine(result4);
    
    Predicate<string> myPredicate2 = s => !string.IsNullOrEmpty(s);
    bool result5 = myPredicate2(null); // Or "A string"
    Console.WriteLine(result5);

1.3. Anonymous delegates

.NET Framework 2.0 introduced the concept of anonymous delegates (more accurately: anonymous methods) to create "inline" delegates without having to specify any additional type or method. [3]

List<int> nums = new List<int>([1, 5, 3, 2, 0, 4]);
nums.Sort(delegate (int x, int y) { return y - x; });
nums.ForEach(delegate (int num) { Console.Write($"{num} "); });
// 5 4 3 2 1 0

Func<int, int, int> add = delegate (int a, int b) { return a + b; };
add(1, 1); // 2

2. Lambda expressions

Lambda expressions, or just "lambdas" for short, were introduced in C# 3.0 as one of the core building blocks of Language Integrated Query (LINQ). [3] Lambda expressions

List<int> nums = new List<int>([1, 5, 3, 2, 0, 4]);
nums.Sort((x, y) => y - x);
nums.ForEach(num => Console.Write($"{num} "));
// 5 4 3 2 1 0
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
    // await Task.Delay(100); // 1 2 3 4 5
    var task = Task.Run(() =>
    {
        Console.Write($"{i} ");
    });
    tasks.Add(task);
    // await Task.Delay(100); // 0 1 2 3 4
}

await Task.WhenAll(tasks);
// 5 5 5 5 5

3. Events

Events in .NET are based on the delegate model that follows the observer design pattern, which enables a subscriber to register with and receive notifications from a provider. [5]

  • To define an event, use the C# event or the Visual Basic Event keyword in the signature of your event class, and specify the type of delegate for the event.

  • To raise an event, add a method that is marked as protected and virtual (in C#) or Protected and Overridable (in Visual Basic).

    Counter counter = new Counter();
    counter.Callback += Console.WriteLine;
    counter.Count++;
    counter.Count++;
    
    class Counter
    {
        public event Callback? Callback;
    
        private void OnCallback(string message)
        {
            Callback?.Invoke(message);
        }
    
        private int _count;
    
        public int Count
        {
            get => _count;
            set
            {
                if (value != _count)
                {
                    int old = _count;
                    _count = value;
                    OnCallback($"Count was changed from {old} to {_count}.");
                }
            }
        }
    
        /* Create an event using custom accessors (add and remove blocks)
        private Callback _callback; // The underlying delegate
    
        // Event declaration with custom accessors
        public event Callback? CallbackEvent
        {
            add
            {
                _callback += value; // Add the handler
            }
            remove
            {
                _callback -= value; // Remove the handler
            }
        }
        */
    }
    
    // $ dotnet run
    // Count was changed from 0 to 1.
    // Count was changed from 1 to 2.