Delegates and Events
Type-safe function references, callbacks, and the event pattern
Introduction
In this lesson, you'll learn about delegates and events in C#. Coming from JavaScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In JavaScript, you're familiar with type-safe function references, callbacks, and the event pattern.
C# has its own approach to type-safe function references, callbacks, and the event pattern, which we'll explore step by step.
The C# Way
Let's see how C# handles this concept. Here's a typical example:
// Built-in delegate types
Action<string> greet = name => Console.WriteLine($"Hello {name}");
Func<int,int,int> add = (a, b) => a + b;
Predicate<int> isEven = x => x % 2 == 0;
// Multicast delegate (+=)
Action<string> log = Console.WriteLine;
log += s => File.AppendAllText("log.txt", s);
log("test"); // calls both!
// Event pattern
public class Button
{
public event EventHandler<string> Clicked;
public void Click(string label)
=> Clicked?.Invoke(this, label);
}
var btn = new Button();
btn.Clicked += (sender, label) => Console.WriteLine($"Clicked: {label}");
btn.Clicked += (_, label) => SaveToDb(label); // += adds handler
btn.Click("Submit"); // both handlers fire
// Func<T1,T2,...,TResult> — last type is return type
Func<string, int> length = s => s.Length;Comparing to JavaScript
Here's how you might have written similar code in JavaScript:
// Functions are first-class
const add = (a, b) => a + b;
const greet = name => `Hello ${name}`;
// Callbacks
function run(fn, x) { return fn(x); }
run(greet, "Alice"); // "Hello Alice"
// Event emitter pattern
const emitter = new EventEmitter();
emitter.on("data", chunk => console.log(chunk));
emitter.emit("data", "hello");
// Array of callbacks
const handlers = [];
handlers.push(x => console.log(x));
handlers.forEach(h => h("event"));You may be used to different syntax or behavior.
Action<T> = void callback; Func<T,R> = function returning R; Predicate<T> = bool function
You may be used to different syntax or behavior.
Delegates are multicast — += adds handlers, -= removes them
You may be used to different syntax or behavior.
event keyword restricts delegate to += and -= only from outside the class
You may be used to different syntax or behavior.
EventHandler<T> is the standard event delegate: (object sender, T args)
You may be used to different syntax or behavior.
Nullable invoke: Clicked?.Invoke() safely handles no subscribers
Step-by-Step Breakdown
1. Action and Func
Use Action<T> for void callbacks and Func<T,R> for functions that return a value. These replace custom delegate declarations.
const fn = x => x * 2;Func<int,int> fn = x => x * 2;
Action<string> log = Console.WriteLine;2. Multicast Delegates
You can chain multiple functions with +=. When invoked, all functions run in order.
const handlers = [fn1, fn2]; handlers.forEach(h => h(x));Action<string> chain = fn1;
chain += fn2;
chain("hello"); // fn1 and fn2 both called3. Events
The event keyword wraps a delegate. External code can only += and -= handlers — it cannot invoke or replace the delegate.
emitter.on("click", handler);
emitter.emit("click", data);// In class:
public event Action<string> DataReceived;
// Trigger:
DataReceived?.Invoke("payload");
// Subscribe:
obj.DataReceived += data => Console.WriteLine(data);4. Standard EventHandler Pattern
EventHandler<TEventArgs> is the .NET convention for events. TEventArgs should inherit from EventArgs.
emitter.on("change", (oldVal, newVal) => ...)class ChangedEventArgs : EventArgs { public int OldValue, NewValue; }
public event EventHandler<ChangedEventArgs> ValueChanged;
// Fire:
ValueChanged?.Invoke(this, new ChangedEventArgs { OldValue=1, NewValue=2 });Common Mistakes
When coming from JavaScript, developers often make these mistakes:
- Action<T> = void callback; Func<T,R> = function returning R; Predicate<T> = bool function
- Delegates are multicast — += adds handlers, -= removes them
- event keyword restricts delegate to += and -= only from outside the class
Key Takeaways
- Action<T> for void; Func<T1,...,TResult> for returning; Predicate<T> for bool predicates
- Delegates are multicast — += chains handlers, -= removes them
- event restricts external code to only += and -=, preventing accidental .Invoke()
- EventHandler<TEventArgs> is the standard .NET event signature pattern