Delegates and Events
Định nghĩa (Definition)
Một ủy quyền (delegate) là một con trỏ hàm an toàn kiểu (type-safe function pointer) tham chiếu đến một phương thức có chữ ký (signature) cụ thể. Ủy quyền cho phép truyền phương thức làm đối số, lưu trữ tham chiếu phương thức, và xây dựng cơ chế gọi lại (callback mechanism).
Một sự kiện (event) là cơ chế thông báo được xây dựng dựa trên ủy quyền, triển khai mẫu quan sát (observer pattern) hay còn gọi là phát hành/đăng ký (pub/sub). Sự kiện giới hạn mã bên ngoài chỉ có thể đăng ký (subscribe) hoặc hủy đăng ký (unsubscribe) — không thể gọi sự kiện từ bên ngoài lớp khai báo.
Khái niệm cốt lõi (Core Concepts)
Khai báo và Gọi Ủy quyền (Delegate Declaration and Invocation)
// Declare a delegate type
public delegate int MathOperation(int a, int b);
// Methods that match the signature
public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;
// Instantiate and invoke
MathOperation op = Add;
int result = op(3, 4); // result = 7
op = Multiply;
result = op(3, 4); // result = 12
Ủy quyền Đa phát (Multicast Delegates)
Ủy quyền tự nhiên là đa phát (multicast) — chúng giữ một danh sách gọi (invocation list) gồm nhiều phương thức thực thi theo thứ tự.
public delegate void NotifyHandler(string message);
NotifyHandler handler = msg => Console.WriteLine($"Email: {msg}");
handler += msg => Console.WriteLine($"SMS: {msg}");
handler += msg => Console.WriteLine($"Push: {msg}");
handler("Order shipped");
// Output:
// Email: Order shipped
// SMS: Order shipped
// Push: Order shipped
// Inspect invocation list
Delegate[] list = handler.GetInvocationList();
Console.WriteLine(list.Length); // 3
// Remove a handler
handler -= msg => Console.WriteLine($"SMS: {msg}"); // removes first match
Khi gọi ủy quyền đa phát có kiểu trả về khác void, chỉ giá trị trả về của phương thức cuối cùng được trả về. Nên sử dụng kiểu trả về void cho ủy quyền đa phát, hoặc duyệt qua GetInvocationList() thủ công.
Các kiểu Ủy quyền Tích hợp (Built-in Delegate Types)
| Ủy quyền (Delegate) | Chữ ký (Signature) | Trường hợp sử dụng (Use Case) |
|---|---|---|
Action | void() | Gọi lại không đầu vào (No-input callback) |
Action<T> | void(T) | Gọi lại một đầu vào (Single-input callback) |
Action<T1, T2> | void(T1, T2) | Gọi lại nhiều đầu vào (Multi-input callback) (tối đa 16 tham số) |
Func<TResult> | TResult() | Không đầu vào, trả về giá trị (No-input, returns value) |
Func<T, TResult> | TResult(T) | Biến đổi/lọc (Transform/filter) |
Predicate<T> | bool(T) | Kiểm tra đúng/sai (True/false test) |
// Action — void return
Action<string> log = message => Console.WriteLine(message);
log("Hello");
// Func — typed return
Func<int, int, int> add = (a, b) => a + b;
int sum = add(2, 3);
// Predicate — bool return
Predicate<int> isEven = n => n % 2 == 0;
bool result = isEven(4); // true
Phương thức Vô danh (Anonymous Methods)
// Anonymous method using delegate keyword
Func<int, int, int> subtract = delegate(int a, int b)
{
return a - b;
};
Biểu thức Lambda (Lambda Expressions)
Lambda là định nghĩa ủy quyền nội tuyến ngắn gọn sử dụng toán tử =>.
// Expression lambda — single expression
Func<int, int> doubleIt = x => x * 2;
// Statement lambda — block of statements
Action<string> greet = name =>
{
string message = $"Hello, {name}!";
Console.WriteLine(message);
};
// Lambda with multiple parameters
Func<int, int, bool> areEqual = (a, b) => a == b;
// Discard parameters when unused
Action<int, int> logFirst = (_, _) => Console.WriteLine("called");
Bao đóng và Biến bị Nắm bắt (Closures and Captured Variables)
Lambda có thể nắm bắt (capture) biến từ phạm vi bao quanh (enclosing scope). Vòng đời của biến bị nắm bắt được kéo dài để khớp với vòng đời của ủy quyền.
public static Func<int> CreateCounter()
{
int count = 0; // captured variable
return () => ++count; // closure over count
}
var counter = CreateCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3
Nắm bắt biến vòng lặp trong lambda có thể gây hành vi không mong đợi vì lambda nắm bắt biến, không phải giá trị của biến tại thời điểm nắm bắt.
// BUG — all lambdas share the same 'i' variable
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions) action();
// Output: 3, 3, 3 (not 0, 1, 2)
// FIX — introduce a local copy inside the loop
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}
// Output: 0, 1, 2
Từ C# 5 trở đi, biến vòng lặp foreach được nắm bắt đúng cho mỗi lần lặp, nhưng biến vòng lặp for thì không.
Sự kiện (Events)
Sự kiện đóng gói ủy quyền, giới hạn mã bên ngoài chỉ có thể += (đăng ký) và -= (hủy đăng ký).
public class Button
{
// Event declaration using built-in EventHandler
public event EventHandler? Clicked;
public void Click()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
}
// Subscribe
var button = new Button();
button.Clicked += (sender, e) => Console.WriteLine("Button clicked!");
button.Click(); // prints: Button clicked!
Mẫu Sự kiện Tiêu chuẩn (Standard Event Pattern)
Mẫu sự kiện .NET tiêu chuẩn sử dụng lớp con EventArgs tùy chỉnh, phương thức protected virtual để kích hoạt sự kiện, và tuân theo quy ước đặt tên.
// 1. Custom EventArgs
public class OrderCreatedEventArgs : EventArgs
{
public int OrderId { get; }
public string CustomerName { get; }
public decimal Total { get; }
public OrderCreatedEventArgs(int orderId, string customerName, decimal total)
{
OrderId = orderId;
CustomerName = customerName;
Total = total;
}
}
// 2. Publisher class
public class OrderService
{
// Event declaration
public event EventHandler<OrderCreatedEventArgs>? OrderCreated;
// Protected virtual method for raising the event
protected virtual void OnOrderCreated(OrderCreatedEventArgs e)
{
OrderCreated?.Invoke(this, e);
}
public void CreateOrder(int orderId, string customer, decimal total)
{
// ... order creation logic ...
OnOrderCreated(new OrderCreatedEventArgs(orderId, customer, total));
}
}
// 3. Subscriber
public class EmailNotifier
{
public void Subscribe(OrderService service)
{
service.OrderCreated += OnOrderCreated;
}
private void OnOrderCreated(object? sender, OrderCreatedEventArgs e)
{
Console.WriteLine($"Sending email for order {e.OrderId} to {e.CustomerName}");
}
}
Bộ truy cập Sự kiện (Event Accessors - add/remove)
Bạn có thể tùy chỉnh cách lưu trữ người đăng ký bằng cách cung cấp bộ truy cập add và remove tường minh.
public class PropertyChangedEventArgs : EventArgs
{
public string PropertyName { get; }
public PropertyChangedEventArgs(string name) => PropertyName = name;
}
public class ViewModel
{
private EventHandler<PropertyChangedEventArgs>? _propertyChanged;
public event EventHandler<PropertyChangedEventArgs> PropertyChanged
{
add => _propertyChanged += value;
remove => _propertyChanged -= value;
}
protected void OnPropertyChanged(string propertyName) =>
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
So sánh Ủy quyền và Sự kiện (Delegates vs Events)
| Tính năng (Feature) | Ủy quyền (Delegate) | Sự kiện (Event) |
|---|---|---|
| Gọi từ b ên ngoài (External invocation) | Cho phép (Allowed) | Chỉ từ lớp khai báo (Only from declaring class) |
| Gán từ bên ngoài (External assignment) | = cho phép | Chỉ += và -= |
| An toàn null (Null safety) | Phải kiểm tra trước khi gọi | Tương tự, nhưng đã được đóng gói |
| Mục đích (Purpose) | Gọi lại (Callback), mẫu chiến lược (Strategy pattern) | Quan sát/phát hành-đăng ký (Observer/publisher-subscriber) |
| Tương thích giao diện (Interface compatibility) | Có thể nằm trong giao diện | Có thể nằm trong giao diện |
- Ủy quyền (Delegate) — khi cần truyền gọi lại (callback), triển khai mẫu chiến lược (strategy pattern), hoặc cho phép người dùng cung cấp một bộ xử lý duy nhất.
- Sự kiện (Event) — khi nhiều người đăng ký cần phản ứng lại điều gì đó đã xảy ra, và chỉ chủ sở hữu mới được kích hoạt.
- Giao diện (Interface) — khi gọi lại yêu cầu nhiều phương thức hoặc mang thêm nghĩa vụ hợp đồng.
Lỗi thường gặp (Common Pitfalls)
Rò rỉ Bộ nhớ do Bộ xử lý Sự kiện (Event Handler Memory Leaks)
Quên hủy đăng ký sự kiện sẽ giữ người đăng ký sống thông qua tham chiếu ủy quyền, ngăn chặn thu gom rác (garbage collection).
public class LeakExample
{
public void Subscribe(Button button)
{
button.Clicked += OnClick;
}
// If LeakExample is discarded but button still lives,
// the delegate holds a reference to LeakExample — memory leak!
public void Unsubscribe(Button button)
{
button.Clicked -= OnClick; // always unsubscribe
}
private void OnClick(object? sender, EventArgs e) { }
}