C# Delegates and Events
What are Delegates
Delegates are type-safe function pointers. They define a method signature and can hold references to methods matching that signature.
// Declare a delegate type
public delegate int MathOperation(int x, int y);
// Methods matching the signature
public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;
// Use the delegate
MathOperation operation = Add;
int result = operation(5, 3); // 8
operation = Multiply;
result = operation(5, 3); // 15
Built-in Delegate Types
.NET provides generic delegate types that cover most use cases.
Func<T, TResult>
For methods that return a value.
// Func<TResult> - no parameters, returns TResult
Func<int> getNumber = () => 42;
int number = getNumber();
// Func<T, TResult> - one parameter
Func<int, int> square = x => x * x;
int squared = square(5); // 25
// Func<T1, T2, TResult> - two parameters
Func<int, int, int> add = (a, b) => a + b;
int sum = add(3, 4); // 7
// Up to 16 parameters supported
Func<string, int, bool, string> format =
(name, age, active) => $"{name}, {age}, {(active ? "active" : "inactive")}";
Action
For methods that return void.
// Action - no parameters
Action greet = () => Console.WriteLine("Hello!");
greet();
// Action<T> - one parameter
Action<string> log = message => Console.WriteLine($"[LOG] {message}");
log("Application started");
// Action<T1, T2> - two parameters
Action<string, int> repeat = (text, count) =>
{
for (int i = 0; i < count; i++)
Console.WriteLine(text);
};
repeat("Hello", 3);
// Up to 16 parameters supported
Predicate
For methods that return bool (testing a condition).
Predicate<int> isPositive = n => n > 0;
bool result = isPositive(5); // true
bool result2 = isPositive(-3); // false
// Common with collection methods
var numbers = new List<int> { -2, -1, 0, 1, 2 };
var positives = numbers.FindAll(isPositive); // [1, 2]
bool anyPositive = numbers.Exists(isPositive); // true
Comparison
For sorting comparisons.
Comparison<string> byLength = (a, b) => a.Length.CompareTo(b.Length);
var words = new List<string> { "apple", "pie", "banana" };
words.Sort(byLength); // ["pie", "apple", "banana"]
// Or inline
words.Sort((a, b) => b.Length.CompareTo(a.Length)); // Descending
Lambda Expressions
Concise syntax for creating delegate instances inline.
Expression Lambdas
Single expression, return inferred.
Func<int, int> square = x => x * x;
Func<int, int, int> add = (a, b) => a + b;
Func<string, bool> isEmpty = s => string.IsNullOrEmpty(s);
// With explicit types when inference fails
Func<object, string> toString = (object o) => o.ToString() ?? "";
Statement Lambdas
Multiple statements in a block.
Func<int, int> factorial = n =>
{
if (n <= 1) return 1;
int result = 1;
for (int i = 2; i <= n; i++)
result *= i;
return result;
};
Action<string> logWithTimestamp = message =>
{
var timestamp = DateTime.Now.ToString("HH:mm:ss");
Console.WriteLine($"[{timestamp}] {message}");
};
Static Lambdas (C# 9.0)
Prevent accidental capture of variables.
int multiplier = 10;
// Regular lambda - captures multiplier
Func<int, int> withCapture = x => x * multiplier;
// Static lambda - cannot capture, compile error if you try
Func<int, int> noCapture = static x => x * 2;
// Func<int, int> error = static x => x * multiplier; // Error!
Discards in Lambdas
Ignore parameters you don’t need.
// Event handler that ignores sender
button.Click += (_, _) => HandleClick();
// Only need second parameter
Func<int, int, int> second = (_, b) => b;
Natural Types and Attributes (C# 10)
Lambdas can be inferred without explicit delegate types.
// Natural type inference - compiler infers Func/Action
var parse = (string s) => int.Parse(s); // Func<string, int>
var action = () => Console.WriteLine("Hello"); // Action
var predicate = (int n) => n > 0; // Func<int, bool>
// Explicit return type when needed
var choose = object (bool b) => b ? 1 : "one";
// Attributes on lambdas
var handler = [Authorize] (HttpContext ctx) => HandleRequest(ctx);
var validated = [return: NotNull] (string s) => s.Trim();
// Method group with natural type
var write = Console.WriteLine; // Action<string>
Default Parameters in Lambdas (C# 12)
// Lambda with default parameter
var greet = (string name = "World") => $"Hello, {name}!";
Console.WriteLine(greet()); // "Hello, World!"
Console.WriteLine(greet("Alice")); // "Hello, Alice!"
// Multiple defaults
Func<int, int, int> add = (int a, int b = 10) => a + b;
Console.WriteLine(add(5)); // 15
Console.WriteLine(add(5, 3)); // 8
// params in lambdas
var sum = (params int[] numbers) => numbers.Sum();
Console.WriteLine(sum(1, 2, 3, 4, 5)); // 15
Multicast Delegates
Delegates can hold references to multiple methods.
Action<string> log = Console.WriteLine;
log += message => File.AppendAllText("log.txt", message + "\n");
log += message => Debug.WriteLine(message);
// Invokes all three methods
log("Application started");
// Remove a handler
log -= Console.WriteLine;
// Check if empty
if (log != null)
log("Still logging");
// Get invocation list
foreach (var handler in log.GetInvocationList())
{
Console.WriteLine(handler.Method.Name);
}
Multicast with Return Values
Only the last method’s return value is returned.
Func<int> getValue = () => 1;
getValue += () => 2;
getValue += () => 3;
int result = getValue(); // 3 (last one)
// To get all results
var results = getValue.GetInvocationList()
.Cast<Func<int>>()
.Select(f => f())
.ToList(); // [1, 2, 3]
Use Delegates (Func/Action)
- Pass behavior as a parameter (strategies, callbacks, LINQ queries)
- The callback is one-to-one: one caller, one handler
- The caller should be able to invoke the delegate directly
- You're doing functional-style programming
Use Events
- Multiple subscribers may want to respond to something happening
- The publisher shouldn't know who's listening (loose coupling)
- Only the class that owns the event should be able to raise it
- You're implementing the observer/pub-sub pattern
An event can only be invoked by the class that declares it. This encapsulation is the key difference from public delegate fields.
// Delegate as parameter - caller controls when it runs
public void ProcessData(Func<string, bool> filter) { /* ... */ }
// Event - publisher controls when it fires, subscribers just react
public event EventHandler<DataEventArgs> DataReceived;
Events
Events are a way to expose delegate functionality while restricting who can invoke them.
Basic Event Pattern
public class Button
{
// Declare event using EventHandler
public event EventHandler? Clicked;
// Method to raise the event
public void SimulateClick()
{
// Null-safe invocation
Clicked?.Invoke(this, EventArgs.Empty);
}
}
// Subscribe to event
var button = new Button();
button.Clicked += (sender, e) => Console.WriteLine("Button clicked!");
button.Clicked += OnButtonClicked;
void OnButtonClicked(object? sender, EventArgs e)
{
Console.WriteLine("Handler method called");
}
// Unsubscribe
button.Clicked -= OnButtonClicked;
// Trigger event
button.SimulateClick();
Custom Event Arguments
// Custom event args
public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Total { get; }
public DateTime Timestamp { get; }
public OrderEventArgs(int orderId, decimal total)
{
OrderId = orderId;
Total = total;
Timestamp = DateTime.UtcNow;
}
}
// Publisher
public class OrderService
{
public event EventHandler<OrderEventArgs>? OrderPlaced;
public event EventHandler<OrderEventArgs>? OrderShipped;
public void PlaceOrder(int orderId, decimal total)
{
// Process order...
OnOrderPlaced(new OrderEventArgs(orderId, total));
}
protected virtual void OnOrderPlaced(OrderEventArgs e)
{
OrderPlaced?.Invoke(this, e);
}
}
// Subscriber
var service = new OrderService();
service.OrderPlaced += (sender, e) =>
{
Console.WriteLine($"Order {e.OrderId} placed for ${e.Total}");
};
Event Accessors
Control how handlers are added/removed.
public class SecurePublisher
{
private EventHandler<EventArgs>? eventHandlers;
private readonly object lockObject = new();
public event EventHandler<EventArgs> SecureEvent
{
add
{
lock (lockObject)
{
eventHandlers += value;
Console.WriteLine("Handler added");
}
}
remove
{
lock (lockObject)
{
eventHandlers -= value;
Console.WriteLine("Handler removed");
}
}
}
protected void OnSecureEvent()
{
EventHandler<EventArgs>? handlers;
lock (lockObject)
{
handlers = eventHandlers;
}
handlers?.Invoke(this, EventArgs.Empty);
}
}
Delegate Patterns
Callback Pattern
public class DataLoader
{
public void LoadDataAsync(
string url,
Action<string> onSuccess,
Action<Exception>? onError = null)
{
try
{
var data = FetchData(url);
onSuccess(data);
}
catch (Exception ex)
{
onError?.Invoke(ex);
}
}
}
// Usage
loader.LoadDataAsync(
"https://api.example.com/data",
data => Console.WriteLine($"Loaded: {data}"),
error => Console.WriteLine($"Error: {error.Message}"));
Strategy Pattern with Delegates
public class PriceCalculator
{
private readonly Func<decimal, decimal> discountStrategy;
public PriceCalculator(Func<decimal, decimal> discountStrategy)
{
this.discountStrategy = discountStrategy;
}
public decimal CalculatePrice(decimal basePrice)
{
return discountStrategy(basePrice);
}
}
// Different strategies
Func<decimal, decimal> noDiscount = price => price;
Func<decimal, decimal> tenPercent = price => price * 0.9m;
Func<decimal, decimal> bulkDiscount = price => price > 100 ? price * 0.8m : price;
var calculator = new PriceCalculator(tenPercent);
decimal finalPrice = calculator.CalculatePrice(50m); // 45
Factory with Delegates
public class ServiceFactory
{
private readonly Dictionary<string, Func<IService>> factories = new();
public void Register(string name, Func<IService> factory)
{
factories[name] = factory;
}
public IService Create(string name)
{
if (factories.TryGetValue(name, out var factory))
return factory();
throw new ArgumentException($"Unknown service: {name}");
}
}
// Registration
factory.Register("email", () => new EmailService());
factory.Register("sms", () => new SmsService());
// Usage
var service = factory.Create("email");
Lazy Evaluation
public class LazyValue<T>
{
private readonly Func<T> factory;
private T? value;
private bool hasValue;
public LazyValue(Func<T> factory)
{
this.factory = factory;
}
public T Value
{
get
{
if (!hasValue)
{
value = factory();
hasValue = true;
}
return value!;
}
}
}
// Usage - factory only called on first access
var lazy = new LazyValue<ExpensiveObject>(() => new ExpensiveObject());
var obj = lazy.Value; // Created here
var obj2 = lazy.Value; // Same instance
Event Best Practices
Thread-Safe Event Raising
public class Publisher
{
public event EventHandler<EventArgs>? SomethingHappened;
protected virtual void OnSomethingHappened()
{
// Capture to local variable for thread safety
var handler = SomethingHappened;
handler?.Invoke(this, EventArgs.Empty);
}
}
Weak Event Pattern
Prevent memory leaks from event subscriptions.
// Using WeakEventManager (WPF) or custom implementation
public class WeakEventSource<TEventArgs> where TEventArgs : EventArgs
{
private readonly List<WeakReference<EventHandler<TEventArgs>>> handlers = new();
public void Subscribe(EventHandler<TEventArgs> handler)
{
handlers.Add(new WeakReference<EventHandler<TEventArgs>>(handler));
}
public void Raise(object sender, TEventArgs args)
{
handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
foreach (var weakRef in handlers.ToList())
{
if (weakRef.TryGetTarget(out var handler))
{
handler(sender, args);
}
}
}
}
Unsubscribe Pattern
public class Subscriber : IDisposable
{
private readonly Publisher publisher;
public Subscriber(Publisher publisher)
{
this.publisher = publisher;
publisher.DataReceived += OnDataReceived;
}
private void OnDataReceived(object? sender, DataEventArgs e)
{
// Handle event
}
public void Dispose()
{
publisher.DataReceived -= OnDataReceived;
}
}
// Use with using
using var subscriber = new Subscriber(publisher);
// Automatically unsubscribes when disposed
Covariance and Contravariance
Delegate Covariance (Return Types)
public class Animal { }
public class Dog : Animal { }
// Covariance - can return more derived type
Func<Animal> animalFactory = () => new Dog();
Animal animal = animalFactory();
Delegate Contravariance (Parameters)
// Contravariance - can accept more general type
Action<Dog> dogAction = (Animal a) => Console.WriteLine(a.GetType());
dogAction(new Dog());
Version History
| Feature | Version | Significance |
|---|---|---|
| Delegates | C# 1.0 | Type-safe function pointers |
| Anonymous methods | C# 2.0 | Inline delegate creation |
| Lambda expressions | C# 3.0 | Concise syntax |
| Func/Action generics | C# 3.0 | Built-in delegate types |
| Variance in delegates | C# 4.0 | Covariance/contravariance |
| Static lambdas | C# 9.0 | Prevent variable capture |
| Natural lambda types | C# 10 | Type inference for lambdas |
| Lambda attributes | C# 10 | Attributes on lambda expressions |
| Default lambda parameters | C# 12 | Optional parameters in lambdas |
| params in lambdas | C# 12 | Variable arguments in lambdas |
Key Takeaways
Use built-in delegates: Prefer Func<>, Action<>, and Predicate<> over custom delegate types.
Events for pub-sub: Use events when multiple subscribers need to respond to something happening.
Lambdas for inline logic: Use lambda expressions for short, focused delegate implementations.
Unsubscribe to prevent leaks: Always unsubscribe from events when the subscriber is disposed.
Thread-safe event raising: Copy the event to a local variable before null-checking and invoking.
Static lambdas for performance: Use static keyword when you don’t need to capture variables to avoid allocations.
Found this guide helpful? Share it with your team:
Share on LinkedIn