Chuyển tới nội dung chính

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
Giá trị trả về của Ủy quyền Đa phát (Multicast Return Values)

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)
Actionvoid()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
Bao đóng trên Biến Vòng lặp (Closure Over Loop Variable)

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 addremove 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épChỉ +=-=
An toàn null (Null safety)Phải kiểm tra trước khi gọiTươ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ệnCó thể nằm trong giao diện
Khi nào sử dụng cái nào (When to Use What)
  • Ủ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) { }
}

Gọi Ủy quyền trên Null (Delegate Invocation on Null)

// Throws NullReferenceException if no handlers
MyDelegate handler = null;
handler(); // CRASH!

// Safe invocation patterns
handler?.Invoke(); // null-conditional (preferred)
handler?.DynamicInvoke(); // alternative

Mẫu Sự kiện Yếu (Weak Event Patterns)

Đối với nhà phát hành sống lâu (long-lived publishers) với người đăng ký sống ngắn (short-lived subscribers), hãy cân nhắc sử dụng WeakEventManager (WPF) hoặc mẫu tham chiếu yếu (weak reference pattern) để tránh phải hủy đăng ký thủ công.

Tóm tắt要点 (Key Takeaways)

  • Ủy quyền (Delegates) là con trỏ hàm an toàn kiểu; sự kiện (events) là ủy quyền bị giới hạn triển khai mẫu quan sát (observer pattern).
  • Sử dụng Action, Func, và Predicate thay vì kiểu ủy quyền tùy chỉnh khi có thể.
  • Lambdabao đóng (closures) rất mạnh — hãy cẩn thận với biến vòng lặp bị nắm bắt.
  • Luôn hủy đăng ký (unsubscribe) sự kiện để ngăn rò rỉ bộ nhớ.
  • Tuân theo mẫu sự kiện tiêu chuẩn (standard event pattern) (EventArgs subclass, protected virtual OnXxx, event EventHandler<T>) cho API công khai.
  • Sử dụng gọi có điều kiện null (null-conditional invocation) (event?.Invoke(...)) để tránh NullReferenceException.

Câu hỏi Phỏng vấn (Interview Questions)

Câu hỏi: Ủy quyền (delegate) là gì? Một con trỏ hàm an toàn kiểu (type-safe function pointer) tham chiếu đến phương thức có chữ ký khớp. Ủy quyền cho phép truyền phương thức làm tham số, cơ chế gọi lại (callback), và gọi đa phát (multicast invocation).

Câu hỏi: Sự khác biệt giữa ủy quyền (delegate) và sự kiện (event) là gì? Sự kiện là ủy quyền được bao bọc bởi các giới hạn truy cập. Mã bên ngoài chỉ có thể đăng ký (+=) hoặc hủy đăng ký (-=) sự kiện, nhưng không thể gọi hoặc thay thế nó. Ủy quyền thông thường có thể được gọi và gán lại từ bên ngoài.

Câu hỏi: ActionFunc là gì? Action là ủy quyền tích hợp cho phương thức trả về void. Func<T, TResult> là ủy quyền tích hợp cho phương thức trả về giá trị. Cả hai hỗ trợ tối đa 16 tham số kiểu.

Câu hỏi: Bao đóng (closure) là gì? Bao đóng xảy ra khi biểu thức lambda nắm bắt 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.

Câu hỏi: Ủy quyền đa phát (multicast delegate) là gì? Ủy quyền giữ tham chiếu đến nhiều phương thức trong danh sách gọi (invocation list). Khi được gọi, tất cả phương thức thực thi tuần tự. Chỉ giá trị trả về cuối cùng được trả về cho ủy quyền có kiểu trả về khác void.

Câu hỏi: Tại sao nên hủy đăng ký (unsubscribe) sự kiện? Bộ xử lý sự kiện (event handler) giữ tham chiếu mạnh (strong reference) đến người đăng ký. Nếu người đăng ký sống ngắn đăng ký đến nhà phát hành sống lâu mà không hủy đăng ký, người đăng ký không thể bị thu gom rác (garbage collected), gây rò rỉ bộ nhớ (memory leak).

Tài liệu tham khảo (References)