Delegates and Events
Action/Func vs Python callables, and the event notification pattern
Introduction
In this lesson, you'll learn about delegates and events in C#. Coming from Python, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Python, you're familiar with action/func vs python callables, and the event notification pattern.
C# has its own approach to action/func vs python callables, and the event notification 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 (no custom delegate declarations needed)
Func<int, int> double_ = x => x * 2;
Action<string> log = Console.WriteLine;
Predicate<int> isEven = x => x % 2 == 0;
// Higher-order function
int Apply(Func<int, int> fn, int val) => fn(val);
Apply(double_, 5); // 10
Apply(x => x + 1, 5); // 6
// Multicast delegate — += chains handlers
Action<string> handlers = Console.WriteLine;
handlers += s => File.AppendAllText("log.txt", s);
handlers("event"); // both run
// Event pattern — proper encapsulation
public class Button
{
// event restricts external code to += and -=
public event Action<string>? Clicked;
public void Click(string label)
=> Clicked?.Invoke(label); // null-safe invoke
}
var btn = new Button();
btn.Clicked += label => Console.WriteLine($"Clicked: {label}");
btn.Clicked += SaveToDb; // += adds another handler
btn.Click("Submit"); // both handlers fireComparing to Python
Here's how you might have written similar code in Python:
from typing import Callable
# Functions as values
def double(x: int) -> int:
return x * 2
# Higher-order functions
def apply(fn: Callable[[int], int], val: int) -> int:
return fn(val)
apply(double, 5) # 10
apply(lambda x: x + 1, 5) # 6
# Callback list pattern
handlers: list[Callable[[str], None]] = []
handlers.append(print)
handlers.append(lambda s: open("log.txt","a").write(s))
for h in handlers:
h("event")
# Observer pattern
class Button:
def __init__(self):
self._on_click: list[Callable[[str], None]] = []
def on_click(self, handler: Callable[[str], None]):
self._on_click.append(handler)
def click(self, label: str):
for h in self._on_click:
h(label)
btn = Button()
btn.on_click(lambda l: print(f"Clicked: {l}"))
btn.click("Submit")You may be used to different syntax or behavior.
Action<T> = void callback (Callable[[T], None]); Func<T,R> = returning function
You may be used to different syntax or behavior.
Delegates are multicast — += chains multiple handlers (Python: list of callables)
You may be used to different syntax or behavior.
event keyword prevents external code from invoking or replacing the delegate
You may be used to different syntax or behavior.
Clicked?.Invoke() safely handles no subscribers (vs Python: if handlers: ...)
You may be used to different syntax or behavior.
Lambda syntax: x => expr in C# vs lambda x: expr in Python
Step-by-Step Breakdown
1. Action and Func
Use built-in Action/Func types instead of custom delegates. Action is void; Func's last type parameter is the return type.
fn: Callable[[int], int]
void_fn: Callable[[str], None]Func<int, int> fn; // int → int
Action<string> voidFn; // string → void
Predicate<int> pred; // int → bool2. Multicast Delegates
+= chains multiple function references. All are called when the delegate is invoked — like calling all functions in a Python list.
handlers = [fn1, fn2]
for h in handlers: h(x)Action<string> chain = fn1;
chain += fn2;
chain("hello"); // fn1 and fn2 called3. Events
The event keyword wraps a delegate for safe publish/subscribe. External code can only += (subscribe) and -= (unsubscribe).
class Emitter:
def on(self, handler): self._h.append(handler)
def emit(self, data): [h(data) for h in self._h]class Emitter {
public event Action<string>? DataReceived;
public void Emit(string data) => DataReceived?.Invoke(data);
}
var e = new Emitter();
e.DataReceived += Console.WriteLine;4. Standard EventHandler Pattern
EventHandler<TEventArgs> is the .NET convention. TEventArgs inherits from EventArgs. Sender is the object that raised the event.
def on_change(old, new): ...class ChangedArgs : EventArgs { public int Old, New; }
public event EventHandler<ChangedArgs>? Changed;
Changed?.Invoke(this, new ChangedArgs { Old=1, New=2 });Common Mistakes
When coming from Python, developers often make these mistakes:
- Action<T> = void callback (Callable[[T], None]); Func<T,R> = returning function
- Delegates are multicast — += chains multiple handlers (Python: list of callables)
- event keyword prevents external code from invoking or replacing the delegate
Key Takeaways
- Action<T> for void callbacks; Func<T,R> for returning functions — no custom delegate declarations
- Multicast: += adds handlers, -= removes; invoke calls all in order
- event keyword: only += and -= allowed from outside class — use for public notifications
- EventHandler<TEventArgs> is the .NET convention for (sender, args) events