Table of Contents
- What Are Delegates?
- Types of Delegates
- Working with Delegates: Declaration, Instantiation, and Invocation
- Anonymous Methods and Lambda Expressions
- What Are Events?
- Event Declaration and Raising
- The Standard Event Pattern: Sender and EventArgs
- Practical Examples
- Example 1: Temperature Sensor Notification System
- Example 2: File System Watcher
- Best Practices for Delegates and Events
- Conclusion
- References
1. What Are Delegates?
A delegate is a type-safe reference to a method. Think of it as a “variable that holds a method”—it allows you to pass methods as parameters, store them in collections, or invoke them dynamically. Unlike raw function pointers in languages like C/C++, delegates in C# are type-safe: they enforce that the method signature (return type and parameters) matches the delegate’s definition.
Key Characteristics:
- Type Safety: The delegate’s signature (return type + parameters) must exactly match the method it references.
- Multicast Support: A delegate can reference multiple methods (via
+=), forming a delegate chain. Invoking the delegate invokes all methods in the chain sequentially. - Inheritance: Delegates inherit from
System.Delegate, but you rarely interact with this base class directly.
2. Types of Delegates
C# provides built-in delegate types for common scenarios, reducing the need to define custom delegates. Let’s explore the most widely used ones:
2.1 Custom Delegates
Defined using the delegate keyword, custom delegates are useful when you need a specific signature not covered by built-in types.
Syntax:
delegate returnType DelegateName(parameterTypes);
Example:
// Delegate for a method that takes two integers and returns an integer
delegate int MathOperation(int a, int b);
2.2 Built-in Delegates
Func<TResult> and Func<T1, T2, ..., TResult>
- Used for methods that return a value.
Func<TResult>: No input parameters, returnsTResult.Func<T1, TResult>: One input parameter (T1), returnsTResult.- Supports up to 16 input parameters (e.g.,
Func<T1, T2, T3, TResult>).
Example:
// A Func that takes two ints and returns their sum (int)
Func<int, int, int> add = (a, b) => a + b;
int result = add(3, 5); // Result: 8
Action<T1, T2, ...>
- Used for methods that return
void. Action: No input parameters.Action<T>: One input parameter (T).- Supports up to 16 input parameters.
Example:
// An Action that prints a string
Action<string> printMessage = (message) => Console.WriteLine(message);
printMessage("Hello, Delegates!"); // Output: Hello, Delegates!
Predicate<T>
- Specialized
Func<T, bool>: Takes one parameter (T) and returns abool. - Common for filtering or validation logic.
Example:
// A Predicate to check if a number is even
Predicate<int> isEven = (num) => num % 2 == 0;
bool result = isEven(4); // Result: true
3. Working with Delegates: Declaration, Instantiation, and Invocation
Let’s break down the lifecycle of a delegate with a custom delegate example.
Step 1: Declare the Delegate
Define the delegate type with the desired signature:
// Delegate for a method that logs a message (string) and returns void
delegate void Logger(string message);
Step 2: Instantiate the Delegate
Assign a method (static or instance) that matches the delegate’s signature:
class LoggingService
{
public static void ConsoleLog(string message)
{
Console.WriteLine($"[Console] {message}");
}
public void FileLog(string message)
{
// Simulate writing to a file
Console.WriteLine($"[File] {message}");
}
}
// Instantiate delegate with static method
Logger consoleLogger = LoggingService.ConsoleLog;
// Instantiate delegate with instance method
var fileLoggerInstance = new LoggingService();
Logger fileLogger = fileLoggerInstance.FileLog;
Step 3: Invoke the Delegate
Call the delegate like a method, or use the Invoke() method explicitly:
consoleLogger("System started"); // Output: [Console] System started
fileLogger.Invoke("User logged in"); // Output: [File] User logged in
Multicast Delegates
A delegate can reference multiple methods (a “multicast” delegate). Use += to add methods and -= to remove them:
// Combine delegates into a multicast delegate
Logger multiLogger = consoleLogger;
multiLogger += fileLogger; // Add fileLogger to the chain
// Invoke all methods in the chain
multiLogger("Data saved");
// Output:
// [Console] Data saved
// [File] Data saved
// Remove a method
multiLogger -= consoleLogger;
multiLogger("Error occurred"); // Output: [File] Error occurred
Note: If a multicast delegate has a return type, only the last method’s return value is returned. For
voiddelegates, all methods are invoked sequentially.
4. Anonymous Methods and Lambda Expressions
Writing named methods for simple delegates can be verbose. C# offers anonymous methods and lambda expressions to define inline method logic.
Anonymous Methods
Introduced in C# 2.0, anonymous methods use the delegate keyword without a name:
Logger anonymousLogger = delegate(string message)
{
Console.WriteLine($"[Anonymous] {message}");
};
anonymousLogger("Using anonymous method"); // Output: [Anonymous] Using anonymous method
Lambda Expressions
Introduced in C# 3.0, lambdas are a more concise syntax for anonymous methods. They use the => (lambda operator) to separate parameters from the body.
Syntax:
(parameters) => expression(single expression, return type inferred).(parameters) => { statements; }(multiple statements, return type explicit if needed).
Examples:
// Lambda with single parameter (omits parentheses)
Logger lambdaLogger1 = message => Console.WriteLine($"[Lambda1] {message}");
// Lambda with explicit parameters and multiple statements
Logger lambdaLogger2 = (message) =>
{
var timestamp = DateTime.Now.ToString("HH:mm:ss");
Console.WriteLine($"[Lambda2] [{timestamp}] {message}");
};
lambdaLogger1("Hello, Lambda!"); // Output: [Lambda1] Hello, Lambda!
lambdaLogger2("With timestamp"); // Output: [Lambda2] [14:30:45] With timestamp
Lambdas are now the preferred way to define inline delegates, especially with LINQ and functional programming patterns.
5. What Are Events?
Events build on delegates to enable publisher-subscriber (pub-sub) communication. They allow a class (the publisher) to notify other classes (subscribers) when a specific action or state change occurs (e.g., a button click, data update, or error).
Key Purpose: Encapsulation
A public delegate field allows any code to invoke it or replace its methods, which is unsafe. Events restrict access:
- Subscribers can only add (
+=) or remove (-=) handlers. - Only the publisher can raise (invoke) the event.
Analogy: Magazine Subscription
- Publisher: A magazine company (decides when to publish issues).
- Event: A “New Issue Released” event.
- Subscribers: Readers who sign up (subscribe) to receive notifications.
- Action: When a new issue is released, the publisher notifies all subscribers.
6. Event Declaration and Raising
To declare and use events, follow these steps:
Step 1: Define a Delegate (or Use Built-in)
Events require a delegate type to define the signature of subscriber methods. Use built-in delegates like Action or EventHandler for simplicity.
Step 2: Declare the Event
Use the event keyword with the delegate type:
public class Button
{
// Event using Action (no parameters)
public event Action Clicked;
}
Step 3: Raise the Event
The publisher raises the event when the action occurs. To ensure safety:
- Check if the event has subscribers (
!= null) before invoking. - Use a protected virtual method to raise the event (allows derived classes to override).
Example:
public class Button
{
// Event declaration
public event Action Clicked;
// Method to simulate a button click (called by the UI, e.g., when the user clicks)
public void Press()
{
Console.WriteLine("Button pressed!");
OnClicked(); // Raise the event
}
// Protected virtual method to raise the event (follows .NET convention)
protected virtual void OnClicked()
{
Clicked?.Invoke(); // Null-conditional operator: invoke only if not null
}
}
Step 4: Subscribe to the Event
Subscribers use += to register handlers (methods or lambdas):
// Subscriber 1: Logs the click
void LogClick()
{
Console.WriteLine("Click logged!");
}
// Subscriber 2: Shows a message (lambda)
var button = new Button();
button.Clicked += LogClick; // Subscribe with method
button.Clicked += () => Console.WriteLine("Button clicked! Hello, User!"); // Subscribe with lambda
// Simulate user clicking the button
button.Press();
// Output:
// Button pressed!
// Click logged!
// Button clicked! Hello, User!
// Unsubscribe when done
button.Clicked -= LogClick;
7. The Standard Event Pattern
For consistency, .NET defines a standard event pattern using EventHandler and EventArgs. This ensures interoperability across libraries and frameworks.
Core Components:
- Delegate:
EventHandler<TEventArgs>(built-in), whereTEventArgsinherits fromSystem.EventArgs. - Sender: The object raising the event (allows subscribers to identify the source).
- EventArgs: A class carrying data about the event (use
EventArgs.Emptyfor no data).
EventHandler Delegate
Defined as:
public delegate void EventHandler(object? sender, EventArgs e);
sender: The publisher instance.e: Event data (derived fromEventArgs).
Custom Event Data with EventArgs
For events needing data, create a subclass of EventArgs:
// Custom EventArgs to carry temperature data
public class TemperatureChangedEventArgs : EventArgs
{
public double NewTemperature { get; }
public double OldTemperature { get; }
public TemperatureChangedEventArgs(double oldTemp, double newTemp)
{
OldTemperature = oldTemp;
NewTemperature = newTemp;
}
}
Applying the Pattern
public class Thermometer
{
private double _temperature;
// Event using EventHandler<TemperatureChangedEventArgs>
public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;
public double Temperature
{
get => _temperature;
set
{
if (_temperature != value)
{
var args = new TemperatureChangedEventArgs(_temperature, value);
_temperature = value;
OnTemperatureChanged(args); // Raise the event
}
}
}
// Protected virtual method to raise the event
protected virtual void OnTemperatureChanged(TemperatureChangedEventArgs e)
{
TemperatureChanged?.Invoke(this, e); // "this" is the sender
}
}
8. Practical Examples
Let’s solidify these concepts with real-world scenarios.
Example 1: Temperature Sensor Notification System
Goal: A thermometer (publisher) notifies a display and a logger (subscribers) when the temperature changes.
Step 1: Define EventArgs (from earlier)
public class TemperatureChangedEventArgs : EventArgs { /* ... */ }
Step 2: Implement the Publisher (Thermometer)
public class Thermometer { /* ... */ } // As defined in Section 7
Step 3: Implement Subscribers
public class Display
{
public void OnTemperatureChanged(object? sender, TemperatureChangedEventArgs e)
{
Console.WriteLine($"[Display] Temp changed from {e.OldTemperature}°C to {e.NewTemperature}°C");
}
}
public class Logger
{
public void LogTemperatureChange(object? sender, TemperatureChangedEventArgs e)
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
Console.WriteLine($"[Logger] [{timestamp}] Temp update: {e.OldTemperature} → {e.NewTemperature}");
}
}
Step 4: Connect Pub-Sub
var thermometer = new Thermometer();
var display = new Display();
var logger = new Logger();
// Subscribe
thermometer.TemperatureChanged += display.OnTemperatureChanged;
thermometer.TemperatureChanged += logger.LogTemperatureChange;
// Simulate temperature updates
thermometer.Temperature = 22.5;
// Output:
// [Display] Temp changed from 0°C to 22.5°C
// [Logger] [2024-01-01 10:00:00] Temp update: 0 → 22.5
thermometer.Temperature = 24.0;
// Output:
// [Display] Temp changed from 22.5°C to 24°C
// [Logger] [2024-01-01 10:05:00] Temp update: 22.5 → 24
// Unsubscribe when done
thermometer.TemperatureChanged -= display.OnTemperatureChanged;
Example 2: File System Watcher
Goal: Monitor a directory and trigger events on file creation/deletion.
using System.IO;
public class FileMonitor
{
private readonly FileSystemWatcher _watcher;
// Events for file creation and deletion
public event Action<string>? FileCreated;
public event Action<string>? FileDeleted;
public FileMonitor(string path)
{
_watcher = new FileSystemWatcher(path)
{
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.FileName
};
// Subscribe to FileSystemWatcher's internal events
_watcher.Created += OnFileCreated;
_watcher.Deleted += OnFileDeleted;
}
private void OnFileCreated(object sender, FileSystemEventArgs e)
{
FileCreated?.Invoke(e.FullPath); // Raise our FileCreated event
}
private void OnFileDeleted(object sender, FileSystemEventArgs e)
{
FileDeleted?.Invoke(e.FullPath); // Raise our FileDeleted event
}
}
// Usage
var monitor = new FileMonitor(@"C:\Temp");
monitor.FileCreated += path => Console.WriteLine($"File created: {path}");
monitor.FileDeleted += path => Console.WriteLine($"File deleted: {path}");
// Keep app running to monitor (simulate with Console.ReadKey())
Console.WriteLine("Monitoring C:\\Temp... Press any key to exit.");
Console.ReadKey();
9. Best Practices for Delegates and Events
To avoid common pitfalls and write maintainable code:
1. Use Standard Event Patterns
Adopt EventHandler<TEventArgs> for events to ensure compatibility with .NET libraries and tools (e.g., Visual Studio designers).
2. Prefer Built-in Delegates
Use Func, Action, and EventHandler instead of custom delegates unless a specific signature is needed. This reduces boilerplate.
3. Make Event Handlers Thread-Safe
If events are raised from background threads, ensure subscribers handle thread safety (e.g., use Control.Invoke in WinForms/WPF to update UI).
4. Avoid Blocking in Handlers
Event handlers should execute quickly. Long-running operations block the publisher, leading to poor responsiveness. Offload work to background threads instead.
5. Clean Up Subscriptions
Unsubscribe (-=) from events when subscribers are no longer needed (e.g., in Dispose() or OnDestroy()). Otherwise, the publisher retains references to subscribers, causing memory leaks.
Example with IDisposable:
public class TempDisplay : IDisposable
{
private readonly Thermometer _thermometer;
public TempDisplay(Thermometer thermometer)
{
_thermometer = thermometer;
_thermometer.TemperatureChanged += OnTemperatureChanged;
}
public void Dispose()
{
_thermometer.TemperatureChanged -= OnTemperatureChanged; // Unsubscribe
}
private void OnTemperatureChanged(object? sender, TemperatureChangedEventArgs e) { /* ... */ }
}
6. Validate Event Args
In publishers, validate EventArgs data to ensure consistency (e.g., non-negative temperatures).
10. Conclusion
Delegates and events are cornerstones of C# programming, enabling flexible, event-driven architectures. Delegates provide type-safe function references, while events build on them to enable encapsulated pub-sub communication. By mastering these constructs, you can create modular, maintainable applications that respond dynamically to changes.
Whether you’re building desktop apps, game engines, or backend services, delegates and events empower you to write code that’s both decoupled and responsive.