I. Gọi không đồng bộ bằng Delegat và Thứ Tự Thực Thi
Dưới đây là một ví dụ minh họa cách Timer sử dụng TimerCallback — một delegat — để thực thi mã nền mà không chặn luồng chính:
public class BackgroundTaskManager
{
private readonly List<string> _logEntries = new();
public IReadOnlyList<string> Log => _logEntries.AsReadOnly();
public BackgroundTaskManager()
{
// Hai timer chạy độc lập, không đồng bộ với constructor
new Timer(_ => _logEntries.Add("slow"), null, TimeSpan.FromMilliseconds(2500), TimeSpan.FromMilliseconds(2500));
new Timer(_ => _logEntries.Add("fast"), null, TimeSpan.FromMilliseconds(2000), TimeSpan.FromMilliseconds(2000));
// Đoạn này chạy ngay lập tức — trước khi bất kỳ callback nào được gọi
_logEntries.Add("mid");
}
}
Và phần kiểm thử tương ứng:
static void Main(string[] args)
{
var manager = new BackgroundTaskManager();
Thread.Sleep(3000); // Đợi đủ thời gian để các callback kích hoạt
// Kết quả in ra thường là: "mid", "fast", "slow" (hoặc "fast", "mid", "slow" nếu có race condition nhẹ)
foreach (var entry in manager.Log.Take(3))
Console.WriteLine(entry);
Console.ReadKey();
}
Kết quả thực tế cho thấy "mid" luôn xuất hiện đầu tiên — vì nó nằm trong luồng khởi tạo, trong khi "slow" và "fast" chỉ xuất hiện sau khi timer kích hoạt — chứng minh rõ ràng bản chất không đồng bộ của delegat.
II. Delegat: Một Cơ Chế Trừu Tượng Ở Cấp Độ Hàm
Delegat trong C# không phải là kiểu dữ liệu đặc biệt, mà là một lớp sinh tự động do trình biên dịch tạo ra để đóng gói tham chiếu tới một hoặc nhiều phương thức có cùng chữ ký. Về mặt khái niệm, delegat là sự trừu tượng hóa hành vi ở cấp độ hàm, trái ngược với interface — trừu tượng hóa ở cấp độ đối tượng.
Sự khác biệt then chốt:
- Tính linh hoạt: Một delegat có thể trỏ tới phương thức tĩnh, instance method, anonymous method hoặc lambda — không yêu cầu lớp cụ thể nào "triển khai" nó.
- Mức độ trừu tượng: Interface bắt buộc lớp triển khai phải cung cấp toàn bộ các thành viên được định nghĩa; delegat chỉ quan tâm đến chữ ký — do đó dễ mở rộng hơn (ví dụ: thêm overload mới không ảnh hưởng đến hiện có).
- Ứng dụng điển hình: Delegat thường dùng trong event handling (
EventHandler), callback (Func<T>,Action<T>), và các mẫu như Observer hay Strategy — nơi cần truyền hành vi như dữ liệu.
III. So Sánh Delegat và Interface trong Mẫu Chiến Lược (Strategy Pattern)
Mẫu Strategy nhằm tách biệt thuật toán khỏi logic sử dụng nó. Dưới đây là hai cách triển khai cho bài toán tính thuế:
Cách 1: Dùng interface
public interface ITaxCalculator
{
double Compute(double income);
}
public class PersonalIncomeTax : ITaxCalculator
{
public double Compute(double income) => income * 0.1;
}
public class CorporateTax : ITaxCalculator
{
public double Compute(double income) => income * 0.3;
}
public class TaxProcessor
{
private readonly ITaxCalculator _calculator;
public TaxProcessor(ITaxCalculator calculator) => _calculator = calculator;
public double Calculate(double income) => _calculator.Compute(income);
}
Cách 2: Dùng delegat
public delegate double TaxCalculation(double income);
public class TaxProcessorWithDelegate
{
private readonly TaxCalculation _calculation;
public TaxProcessorWithDelegate(TaxCalculation calculation) => _calculation = calculation;
public double Calculate(double income) => _calculation(income);
}
// Sử dụng:
var processor = new TaxProcessorWithDelegate(income => income * 0.15); // Thuế suất tùy chỉnh
Cả hai cách đều tuân thủ nguyên tắc Open/Closed: dễ mở rộng (thêm chiến lược mới) nhưng không cần sửa code cũ. Delegat phù hợp khi chiến lược đơn giản, vô trạng thái và không cần tái sử dụng across nhiều ngữ cảnh; interface lại ưu tiên khi chiến lược có trạng thái, phụ thuộc vào các service khác hoặc cần kiểm soát vòng đời rõ ràng.
IV. Tối Ưu Hóa Bằng Lambda và Anonymous Method
Thay vì định nghĩa từng phương thức riêng lẻ rồi đăng ký vào delegat, ta có thể dùng lambda để giảm độ dài và tăng tính trực quan:
public class MessageComposer
{
private readonly List<string> _parts = new();
public IReadOnlyList<string> Parts => _parts.AsReadOnly();
public MessageComposer()
{
Action compose = () =>
{
_parts.Add("Hello");
_parts.Add(",");
_parts.Add("World");
};
_parts.Add("Xin chào,");
compose();
}
}
So sánh với cách truyền thống (khai báo riêng hàm Hello(), Split(), World() rồi dùng +=), cách trên loại bỏ hoàn toàn boilerplate, tập trung vào logic nghiệp vụ — đặc biệt hữu ích trong các kịch bản ngắn hạn như xử lý callback, event handler nội bộ hoặc pipeline xử lý dữ liệu.