Delegates and Events
Delegate Concepts
A delegate is a type-safe function pointer in C# that allows methods to be passed as parameters. A delegate defines a method's signature (return type and parameter list), and only methods matching that signature can be assigned to a delegate instance.
Unlike C/C++ function pointers, delegates are object-oriented, type-safe, and support multicast invocation.
Declaring and Using Delegates
Use the delegate keyword to declare a delegate type, then create an instance and associate it with a method.
Example
delegate int MathOp(int a, int b);
class Program
{
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static void Main()
{
MathOp op = Add;
Console.WriteLine(op(3, 4));
op = Multiply;
Console.WriteLine(op(3, 4));
}
}
7
12
Multicast Delegates
Delegates support multicast, meaning a single delegate instance can reference multiple methods. Use += to add a method and -= to remove one. When invoked, all methods are executed in the order they were added. For delegates with return values, a multicast invocation only returns the result of the last method.
Example
delegate void NotifyHandler(string msg);
class Program
{
static void LogToConsole(string msg) => Console.WriteLine($"Console: {msg}");
static void LogToFile(string msg) => Console.WriteLine($"File: {msg}");
static void LogToDb(string msg) => Console.WriteLine($"Database: {msg}");
static void Main()
{
NotifyHandler handler = LogToConsole;
handler += LogToFile;
handler += LogToDb;
handler("System started");
Console.WriteLine("---");
handler -= LogToFile;
handler("System shutdown");
}
}
Console: System started
File: System started
Database: System started
---
Console: System shutdown
Database: System shutdown
Built-in Delegates
C# provides three commonly used built-in delegates that cover most scenarios without requiring custom definitions.
Action
Action represents a method with no return value. It supports 0 to 16 generic parameters: Action, Action<T>, Action<T1,T2>, etc.
Example
class Program
{
static void Main()
{
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice");
Action noParam = () => Console.WriteLine("Parameterless Action");
noParam();
}
}
Hello, Alice!
Parameterless Action
Func
Func<T, TResult> represents a method with a return value. The last generic parameter is the return type, and the preceding ones are input parameters: Func<TResult>, Func<T, TResult>, Func<T1,T2,TResult>, etc.
Example
class Program
{
static void Main()
{
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(10, 20));
Func<string> getTime = () => DateTime.Now.ToString("HH:mm:ss");
Console.WriteLine(getTime());
}
}
30
14:30:00
Predicate
Predicate<T> represents a method that returns bool, commonly used for filtering and conditional checks.
Example
class Program
{
static void Main()
{
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4));
Console.WriteLine(isEven(7));
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var evens = numbers.FindAll(isEven);
Console.WriteLine(string.Join(", ", evens));
}
}
True
False
2, 4, 6
Anonymous Methods
Anonymous methods use the delegate keyword to define an inline code block without needing a separate named method.
Example
class Program
{
static void Main()
{
Func<int, int> square = delegate(int x) { return x * x; };
Console.WriteLine(square(5));
Action<string> log = delegate(string msg) { Console.WriteLine($"[LOG] {msg}"); };
log("Anonymous method example");
}
}
25
[LOG] Anonymous method example
x => x * x for their more concise syntax.
Events
An event is a publish-subscribe mechanism based on delegates. When a delegate field is modified with the event keyword, only the class that declares the event can raise it internally; external classes can only subscribe or unsubscribe using += and -=, and cannot invoke it directly.
Example
class Button
{
public event EventHandler<string> OnClick;
public void Click()
{
OnClick?.Invoke(this, "Button clicked");
}
}
class Program
{
static void Main()
{
var btn = new Button();
btn.OnClick += (sender, msg) => Console.WriteLine($"Event received: {msg}");
btn.Click();
btn.Click();
}
}
Event received: Button clicked
Event received: Button clicked
OnClick.Invoke() directly will throw a NullReferenceException, so you should use the null-conditional invocation ?.Invoke().
Publish-Subscribe Pattern
The publish-subscribe pattern (Publisher-Subscriber) is the core application scenario for events. The publisher is responsible for raising events, and the subscribers are responsible for responding to them — the two are completely decoupled.
Example
class NewsPublisher
{
public event EventHandler<string> OnNewsPublished;
public void Publish(string news)
{
Console.WriteLine($"Publishing news: {news}");
OnNewsPublished?.Invoke(this, news);
}
}
class NewsSubscriber
{
public string Name { get; }
public NewsSubscriber(string name)
{
Name = name;
}
public void Subscribe(NewsPublisher publisher)
{
publisher.OnNewsPublished += HandleNews;
}
public void Unsubscribe(NewsPublisher publisher)
{
publisher.OnNewsPublished -= HandleNews;
}
private void HandleNews(object sender, string news)
{
Console.WriteLine($" [{Name}] Received: {news}");
}
}
class Program
{
static void Main()
{
var publisher = new NewsPublisher();
var sub1 = new NewsSubscriber("Reader A");
var sub2 = new NewsSubscriber("Reader B");
sub1.Subscribe(publisher);
sub2.Subscribe(publisher);
publisher.Publish("C# 12 officially released");
sub2.Unsubscribe(publisher);
publisher.Publish(".NET 9 performance improved by 50%");
}
}
Publishing news: C# 12 officially released
[Reader A] Received: C# 12 officially released
[Reader B] Received: C# 12 officially released
Publishing news: .NET 9 performance improved by 50%
[Reader A] Received: .NET 9 performance improved by 50%
Custom EventArgs
The T in EventHandler<T> typically inherits from EventArgs and is used to carry custom event data.
Example
class TemperatureEventArgs : EventArgs
{
public double Temperature { get; }
public DateTime Timestamp { get; }
public TemperatureEventArgs(double temperature)
{
Temperature = temperature;
Timestamp = DateTime.Now;
}
}
class TemperatureMonitor
{
public event EventHandler<TemperatureEventArgs> OnTemperatureAlert;
private double _threshold;
public TemperatureMonitor(double threshold)
{
_threshold = threshold;
}
public void CheckTemperature(double current)
{
Console.WriteLine($"Current temperature: {current}°C");
if (current > _threshold)
{
var args = new TemperatureEventArgs(current);
OnTemperatureAlert?.Invoke(this, args);
}
}
}
class Cooler
{
public void StartCooling(object sender, TemperatureEventArgs e)
{
Console.WriteLine($" 🚨 Temperature exceeded! {e.Temperature}°C > threshold, starting cooling (time: {e.Timestamp:HH:mm:ss})");
}
}
class Program
{
static void Main()
{
var monitor = new TemperatureMonitor(30.0);
var cooler = new Cooler();
monitor.OnTemperatureAlert += cooler.StartCooling;
monitor.CheckTemperature(25.0);
monitor.CheckTemperature(32.5);
monitor.CheckTemperature(28.0);
monitor.CheckTemperature(35.0);
}
}
Current temperature: 25°C
Current temperature: 32.5°C
🚨 Temperature exceeded! 32.5°C > threshold, starting cooling (time: 14:30:00)
Current temperature: 28°C
Current temperature: 35°C
🚨 Temperature exceeded! 35°C > threshold, starting cooling (time: 14:30:01)
Delegates vs Events
| Feature | Delegate | Event |
|---|---|---|
| Nature | Type (function pointer) | Modifier (restricted delegate instance) |
| External invocation | Can be invoked anywhere | Only the declaring class can raise it internally |
| External assignment | Can be overwritten with = |
Can only use += / -= |
| Usage | Callbacks, strategy pattern | Publish-subscribe, notification mechanisms |
| Safety | Lower | Higher, encapsulates invocation permissions |
event modifier applied, which restricts external access to invocation and assignment, ensuring that only the publisher can raise the event.
❓ FAQ
event modifier applied; external code can only subscribe/unsubscribe using +=/-=, cannot invoke or overwrite it directly, and only the declaring class can raise it internally.?.Invoke() to raise an event?NullReferenceException when the event has no subscribers; ?.Invoke() automatically skips the call when the event is null.Predicate<T> is a specialized version of Func<T, bool> with more explicit semantics, commonly used in filtering scenarios.📖 Summary
- Delegates are type-safe function pointers that define method signatures
- Use the
delegatekeyword to declare custom delegate types - Multicast delegates combine multiple methods via
+=/-=, executing them in order Actionhas no return value,Funchas a return value,Predicatereturns bool- Anonymous methods use
delegate{ }for inline definition; lambda expressions are a more concise alternative - The
eventkeyword restricts delegate invocation and assignment permissions, allowing only the declaring class to raise it - The publish-subscribe pattern decouples publishers from subscribers
- Custom EventArgs inherit from the EventArgs base class to carry event data
📝 Exercises
- Define a
delegate string Transform(string s);delegate, associate it with three methods for converting to uppercase, converting to lowercase, and reversing the string, then test multicast invocation - Use
Func<int, int, bool>to implement a comparator that determines whether the first number is greater than the second - Create a
FileWatcherclass with anevent EventHandler<string> OnFileChanged, simulating notifications to subscribers when a file changes - Define custom
StockPriceEventArgs(containing stock symbol, price, and change percentage), and implement a stock price monitor that raises an event when the change percentage exceeds a threshold - Compare using a custom delegate versus
EventHandler<T>to implement the same event, and summarize the applicable scenarios for each



