Delegates and Lambdas, Events in .NET
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 theTResult
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 ofFunc
, 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 BasicEvent
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
andvirtual
(in C#) orProtected
andOverridable
(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.
References
-
[1] https://learn.microsoft.com/en-us/dotnet/standard/base-types/common-type-system#delegates
-
[2] https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/using-delegates
-
[3] https://learn.microsoft.com/en-us/dotnet/standard/delegates-lambdas
-
[4] https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions
-
[5] https://learn.microsoft.com/en-us/dotnet/standard/events/