Trong C#, một delegate là một kiểu dữ liệu đặc biệt có khả năng đại diện cho các phương thức có cùng một danh sách tham số và kiểu trả về nhất định. Khi bạn khởi tạo một đối tượng delegate, bạn có thể liên kết nó với bất kỳ phương thức nào có chữ ký (signature) tương thích. Điều này cho phép bạn gọi phương thức đó thông qua đối tượng delegate đã tạo.
Mục đích chính của delegate là cho phép truyền các phương thức như những tham số cho các phương thức khác. Một ví dụ điển hình là các trình xử lý sự kiện (event handlers), nơi các phương thức được gọi thông qua delegate. Bạn có thể xây dựng một phương thức tùy chỉnh mà một lớp, chẳng hạn như điều khiển giao diện người dùng, có thể gọi khi một sự kiện cụ thể xảy ra. Dưới đây là một khai báo delegate minh họa:
public delegate double CalculateOperation(double num1, double num2);
Bạn có thể gán bất kỳ phương thức nào, dù là phương thức tĩnh hay phương thức thể hiện (instance method), từ bất kỳ lớp hoặc cấu trúc nào có thể truy cập được và phù hợp với kiểu delegate cho một đối tượng delegate. Khả năng này mang lại sự linh hoạt để thay đổi cách gọi phương thức theo lập trình và cho phép chèn mã mới vào các lớp hiện có.
Lưu ý quan trọng: Khi nói đến delegate, chữ ký của phương thức bao gồm cả kiểu trả về. Điều này khác với ngữ cảnh nạp chồng phương thức (method overloading), nơi kiểu trả về không được tính vào chữ ký. Điều này có nghĩa là một phương thức và một delegate phải có cùng kiểu trả về để có thể tương thích.
Khả năng tham chiếu một phương thức như một tham số làm cho delegate trở thành lựa chọn lý tưởng để định nghĩa các phương thức callback. Chẳng hạn, một tham chiếu đến một phương thức so sánh hai đối tượng có thể được truyền làm tham số cho một thuật toán sắp xếp. Vì logic so sánh được tách riêng, thuật toán sắp xếp có thể được viết một cách tổng quát hơn.
Đặc điểm của Delegate
Các delegate có những đặc tính sau:
- Chúng tương tự như con trỏ hàm (function pointer) trong C++, nhưng được an toàn về kiểu (type-safe).
- Chúng cho phép truyền phương thức như một tham số.
- Chúng có thể được dùng để định nghĩa các phương thức callback.
- Nhiều delegate có thể được xâu chuỗi (chain) lại với nhau, cho phép gọi nhiều phương thức cho một sự kiện duy nhất.
- Phương thức được gán không cần phải khớp hoàn toàn với kiểu delegate (ví dụ: covariant return types và contravariant argument types được hỗ trợ).
- Một đối tượng delegate thường được tạo bằng cách cung cấp tên của phương thức mà nó sẽ gói gọn hoặc sử dụng phương thức ẩn danh (anonymous method) hoặc biểu thức lambda.
Sau khi được khởi tạo, một delegate sẽ chuyển cuộc gọi phương thức đến phương thức mà nó gói gọn. Các tham số mà bên gọi truyền cho delegate sẽ được chuyển đến phương thức đó, và delegate sẽ trả về giá trị trả về của phương thức (nếu có) cho bên gọi. Quá trình này được gọi là "gọi delegate". Một delegate đã được khởi tạo có thể được gọi giống như chính phương thức mà nó gói gọn. Ví dụ:
// Định nghĩa một delegate
public delegate void NotificationHandler(string infoMessage);
// Một phương thức tĩnh phù hợp với chữ ký của NotificationHandler
public static void DisplayMessage(string content)
{
Console.WriteLine($"[Thông báo]: {content}");
}
// Khởi tạo delegate và gán phương thức
NotificationHandler myHandler = DisplayMessage;
// Gọi delegate
myHandler("Chào thế giới!");
Các kiểu delegate trong C# được kế thừa từ lớp
Vì một đối tượng delegate đã được khởi tạo là một đối tượng, nó có thể được truyền làm tham số hoặc gán cho một thuộc tính. Điều này cho phép một phương thức chấp nhận một delegate làm tham số và gọi delegate đó sau này. Đây là một mô hình phổ biến được gọi là callback không đồng bộ, được dùng để thông báo cho bên gọi khi một quá trình dài kết thúc. Khi delegate được sử dụng theo cách này, mã gọi delegate không cần biết phương thức triển khai cụ thể nào sẽ được sử dụng. Chức năng này tương tự như chức năng được cung cấp bởi một interface gói gọn.
Một trường hợp sử dụng phổ biến khác của callback là định nghĩa một phương thức so sánh tùy chỉnh và truyền delegate đó vào một phương thức sắp xếp. Điều này cho phép mã của bên gọi trở thành một phần của thuật toán sắp xếp. Phương thức ví dụ dưới đây chấp nhận một delegate kiểu
public void ProcessAndNotify(int valA, int valB, NotificationHandler notifier)
{
int sumResult = valA + valB;
notifier($"Kết quả tổng là: {sumResult}");
}
Sau đó, bạn có thể truyền delegate đã tạo ở trên vào phương thức này:
NotificationHandler myHandler = DisplayMessage; // Sử dụng handler đã định nghĩa trước
ProcessAndNotify(5, 7, myHandler);
Bạn sẽ nhận được đầu ra sau trên console:
[Thông báo]: Kết quả tổng là: 12
Khi sử dụng delegate một cách trừu tượng, phương thức
Khi một delegate được xây dựng để gói gọn một phương thức thể hiện, nó sẽ tham chiếu cả thể hiện và phương thức. Delegate không biết về kiểu của thể hiện ngoài phương thức được gói gọn, vì vậy một delegate có thể tham chiếu đến một phương thức trên bất kỳ đối tượng nào miễn là đối tượng đó có một phương thức phù hợp với chữ ký của delegate. Khi một delegate được xây dựng để gói gọn một phương thức tĩnh, nó chỉ tham chiếu đến phương thức. Hãy xem xét khai báo sau:
public class MyService
{
public void LogAction(string description) { Console.WriteLine($"Ghi nhận: {description}"); }
public void TrackEvent(string eventName) { Console.WriteLine($"Theo dõi sự kiện: {eventName}"); }
}
Cùng với phương thức tĩnh
Delegate đa hướng (Multicast Delegates)
Khi được gọi, một delegate có thể gọi nhiều phương thức. Điều này được gọi là đa hướng (multicasting). Để thêm một phương thức khác vào danh sách các phương thức của delegate (gọi là danh sách gọi - invocation list), bạn chỉ cần sử dụng toán tử cộng (
MyService service = new MyService();
NotificationHandler handler1 = service.LogAction;
NotificationHandler handler2 = service.TrackEvent;
NotificationHandler handler3 = DisplayMessage; // Phương thức tĩnh đã định nghĩa trước
// Kết hợp các delegate
NotificationHandler combinedHandlers = handler1;
combinedHandlers += handler2;
combinedHandlers += handler3; // Thêm phương thức tĩnh vào danh sách
Tại thời điểm này, danh sách gọi của
Để xóa một phương thức khỏi danh sách gọi, hãy sử dụng toán tử trừ (
// Xóa phương thức LogAction
combinedHandlers -= handler1;
// Sao chép combinedHandlers đồng thời xóa handler2
NotificationHandler remainingHandler = combinedHandlers - handler2;
Vì các kiểu delegate kế thừa từ
int countInvoked = handler3.GetInvocationList().Length;
Delegate có nhiều phương thức trong danh sách gọi được gọi là
Delegate đa hướng được sử dụng rộng rãi trong xử lý sự kiện. Đối tượng nguồn sự kiện gửi thông báo sự kiện đến các đối tượng nhận đã đăng ký để nhận sự kiện đó. Để đăng ký một sự kiện, bên nhận cần tạo một phương thức để xử lý sự kiện, sau đó tạo một delegate cho phương thức đó và truyền delegate đó đến nguồn sự kiện. Khi sự kiện xảy ra, nguồn sẽ gọi delegate. Delegate sau đó sẽ gọi phương thức xử lý sự kiện trên bên nhận, cung cấp dữ liệu sự kiện. Kiểu delegate cho một sự kiện nhất định được xác định bởi nguồn sự kiện. Để biết thêm thông tin, hãy xem Events (C# Programming Guide).
So sánh hai delegate có kiểu khác nhau trực tiếp tại thời điểm biên dịch sẽ gây ra lỗi biên dịch. Tuy nhiên, nếu thể hiện delegate có kiểu
delegate void ActionDelegateA();
delegate void ActionDelegateB();
static void CompareDelegates(ActionDelegateA delA, ActionDelegateB delB, System.Delegate genericDel)
{
// Lỗi biên dịch: Không thể so sánh các kiểu delegate khác nhau trực tiếp.
// Console.WriteLine(delA == delB);
// Hợp lệ lúc biên dịch. Sẽ là 'false' nếu kiểu thực thi của genericDel
// không giống với kiểu của delA.
Console.WriteLine(delA == genericDel);
}