Delegates and Events
A delegate is a type-safe reference to one or more methods with a matching signature. Where a raw function pointer in C or C++ has no type safety, a C# delegate declares exactly which parameter types and return type it accepts, and the compiler enforces that any method or lambda assigned to it matches. Delegates underpin callbacks, LINQ (Func, Action, Predicate are all delegate types), and the entire event system. An event is a language feature built on top of a delegate that restricts how outside code can interact with it: subscribers may only add (+=) or remove (-=) handlers, while only the declaring class can invoke (raise) the event.
Cricket analogy: A delegate is like a bowling-change slip specifying exactly which bowler type is allowed, unlike a blank slip that accepts anyone; an event is like only the captain being allowed to signal a change while fielders can only request to join the rotation.
Declaring and Invoking Delegates
You declare a custom delegate type with the delegate keyword, or reuse the generic Func<...>/Action<...> families for most cases. A delegate instance can reference a static method, an instance method, or a lambda, and multiple methods can be combined into a single multicast delegate using the + operator; invoking the delegate calls each subscriber in order. If a multicast Func<T> is invoked, only the return value of the last invoked method is observed by the caller — a common trap when combining delegates that return values instead of void.
Cricket analogy: Declaring a delegate is like defining a specific over-signal for a fast bowler only, while Func/Action are like standard umpire signals reused across matches; chaining bowlers into one multicast over means only the last ball's result is what the scoreboard shows.
public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);
public class StockTicker
{
private decimal _price;
// Event built on a delegate; EventHandler<T> is the idiomatic .NET pattern
public event EventHandler<PriceChangedEventArgs>? PriceChanged;
public decimal Price
{
get => _price;
set
{
if (value == _price) return;
var old = _price;
_price = value;
OnPriceChanged(old, value);
}
}
protected virtual void OnPriceChanged(decimal oldPrice, decimal newPrice)
{
// ?.Invoke handles the case where no subscribers exist
PriceChanged?.Invoke(this, new PriceChangedEventArgs(oldPrice, newPrice));
}
}
public sealed class PriceChangedEventArgs(decimal oldPrice, decimal newPrice) : EventArgs
{
public decimal OldPrice { get; } = oldPrice;
public decimal NewPrice { get; } = newPrice;
}
var ticker = new StockTicker();
ticker.PriceChanged += (sender, e) =>
Console.WriteLine($"Price moved from {e.OldPrice} to {e.NewPrice}");
ticker.Price = 101.50m;Why Events Wrap Delegates
Exposing a raw public delegate field would let any external code overwrite all subscribers with = instead of adding with +=, or invoke the delegate directly, breaking encapsulation of who is allowed to raise the notification. The event keyword compiles to a private backing delegate field plus public add/remove accessors, so external code is limited to subscribing and unsubscribing, while only members of the declaring class can call Invoke. The standard .NET convention pairs event EventHandler<TEventArgs> with an OnXxx protected virtual method that derived classes can override to add behavior before/after raising the event.
Cricket analogy: Letting anyone overwrite a public delegate field is like letting any spectator grab the PA mic and cancel the umpire's announcement; the event keyword gives spectators only a request line (+=/-=) while only the umpire raises the actual announcement (OnXxx).
Func<T,...,TResult>, Action<T,...>, and Predicate<T> are simply pre-declared generic delegate types in the BCL. You rarely need to declare a custom delegate type anymore except when you need named, self-documenting parameters (as in PriceChangedHandler above) or ref/out parameters, which Func/Action do not support directly.
Invoking an event without the null-conditional operator (PriceChanged(this, e) instead of PriceChanged?.Invoke(this, e)) throws a NullReferenceException if no subscribers have attached. Always use ?.Invoke, or capture the delegate into a local variable first, to avoid a race where the last subscriber unsubscribes between the null check and the invocation.
- A delegate is a type-safe reference to one or more methods matching a specific signature.
- Func<T,...,TResult>, Action<T,...>, and Predicate<T> cover most delegate needs without a custom declaration.
- Multicast delegates invoke all subscribers in order; only the last subscriber's return value is observed for non-void delegates.
- Events restrict external code to += and -= subscription, while only the declaring type can raise the event via Invoke.
- The standard pattern pairs event EventHandler<TEventArgs> with a protected virtual OnXxx method for extensibility.
- Always invoke events with the ?.Invoke null-conditional pattern to avoid NullReferenceException when there are no subscribers.
Practice what you learned
1. What is a delegate in C#?
2. What happens when you invoke a multicast Func<T> delegate with multiple subscribers?
3. Why does C# restrict outside code to += and -= on an event instead of allowing direct delegate assignment?
4. What is the risk of calling PriceChanged(this, e) directly instead of PriceChanged?.Invoke(this, e)?
5. Which built-in generic delegate type would best fit a callback with no parameters and no return value?
Was this page helpful?
You May Also Like
Lambda Expressions
Lambda expressions provide concise, inline syntax for anonymous functions, forming the backbone of LINQ, delegates, and functional-style C# code.
Interfaces in C#
Understand interfaces as pure behavioral contracts that any type can implement, including default interface methods, explicit implementation, and multiple interface inheritance.
Extension Methods
Extension methods let you add new callable methods to existing types, including sealed or third-party ones, without modifying their source or subclassing.
Encapsulation and Access Modifiers
Understand how C#'s access modifiers — public, private, protected, internal, and their combinations — let you control visibility and enforce encapsulation boundaries.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics