Khái niệm và nguyên lý hoạt động của Delegate
Trong lập trình, Delegate là một đối tượng an toàn kiểu, đóng vai trò tham chiếu đến một phương thức cụ thể. Bản chất của nó là một class đặc biệt kế thừa từ System.MulticastDelegate, cho phép chúng ta lưu trữ địa chỉ của hàm và thực thi động tại thời điểm runtime.Delegate yêu cầu signature của phương thức được trỏ đến phải khớp hoàn toàn về số lượng, thứ tự, kiểu dữ liệu của tham số và kiểu giá trị trả về.
C# cho phép định nghĩa delegate thông qua từ khóa delegate. Một delegate có thể khai báo từ 0 đến tối đa 32 tham số, đồng thời hỗ trợ trả về giá trị hoặc không tùy theo thiết kế. Dưới đây là minh họa các mẫu sử dụng thực tế:
using System;
namespace DelegateDemo
{
class Program
{
// Định nghĩa delegate với signature: nhận string, trả về string
public delegate string TransformRule(string input);
static void Main(string[] args)
{
// 1. Truyền tham chiếu phương thức làm đối số cho hàm khác
ProcessData("raw_value", ConvertToUpper);
// 2. Khởi tạo delegate trực tiếp và gọi thực thi
TransformRule directHandler = ConvertToLower;
Console.WriteLine(directHandler("ORIGINAL_TEXT"));
// 3. Sử dụng anonymous method (phù hợp với các phiên bản C# cũ)
TransformRule anonHandler = delegate(string val)
{
return $"<{val}>";
};
Console.WriteLine(anonHandler("data"));
// 4. Áp dụng Lambda expression (cú pháp ngắn gọn, được khuyến nghị hiện đại)
TransformRule lambdaHandler = (text) => $"*{text.Trim()}*";
Console.WriteLine(lambdaHandler(" sample "));
Console.ReadLine();
}
// Hàm nhận delegate làm tham số để đóng gói logic gọi hàm
public static void ProcessData(string source, TransformRule formatter)
{
Console.WriteLine($"Output: {formatter(source)}");
}
public static string ConvertToUpper(string text) => text.ToUpper();
public static string ConvertToLower(string text) => text.ToLower();
}
}
Các Generic Delegate tích hợp sẵn
Để tối ưu hóa mã nguồn và tránh việc phải tự định nghĩa delegate cho các tác vụ thông thường, thư viện chuẩn .NET cung cấp sẵn ba generic delegate mạnh mẽ: Func, Action và Predicate.
Func<T1...Tn, TResult>
Func được thiết kế cho các thao tác bắt buộc phải trả về kết quả. Kiểu dữ liệu cuối cùng trong danh sách generic luôn đại diện cho giá trị trả về, trong khi các kiểu đứng trước là tham số đầu vào. Generic này hỗ trợ từ 0 đến 16 tham số đầu vào và tuyệt đối không chấp nhận kiểu void.
// Định nghĩa hàm tính diện tích tam giác: nhận 2 số thực, trả về số thực
Func<double, double, double> calculateArea = (baseLen, height) => (baseLen * height) / 2;
Console.WriteLine(calculateArea(5.0, 4.0)); // Kết quả: 10
Action<T1...Tn>
Ngược lại với Func, Action dành riêng cho các phương thức thực thi tác dụng phụ mà không cần trả về giá trị nào. Nó cũng giới hạn tối đa 16 tham số đầu vào.
// Hành động ghi log với thông điệp và mức độ ưu tiên
Action<string, int> logSystem = (message, level) =>
Console.WriteLine($"[LogLevel:{level}] {message}");
logSystem("Database connection failed", 3);
Predicate<T>
Predicate là một trường hợp đặc biệt, được dùng chủ yếu trong các phương thức lọc dữ liệu (như List.Find() hay Array.Find()). Nó luôn chấp nhận đúng một tham số đầu vào và ép buộc giá trị trả về phải là bool.
// Kiểm tra điều kiện chẵn lẻ của số nguyên
Predicate<int> checkEven = number => number % 2 == 0;
Console.WriteLine(checkEven(10)); // True
Console.WriteLine(checkEven(7)); // False