CodeGym /Các khóa học /C# SELF /Delegate: các kịch bản nâng cao

Delegate: các kịch bản nâng cao

C# SELF
Mức độ , Bài học
Có sẵn

1. Callback và lập trình bất đồng bộ

Callback (callback) — cơ chế truyền một method để gọi khi một thao tác kết thúc. Rất thường dùng trong các thao tác bất đồng bộ, timer, xử lý dữ liệu, UI.

Ví dụ 1: Thao tác bất đồng bộ với callback

Giả sử chúng ta có ứng dụng, người dùng nhập truy vấn, kết quả trả về có độ trễ (ví dụ từ internet). Sau khi nhận dữ liệu ta muốn cập nhật màn hình.


// Delegate cho callback
public delegate void DataReceivedHandler(string result);

// Cơ chế tải dữ liệu bất đồng bộ (mô phỏng)
public void DownloadDataAsync(DataReceivedHandler callback)
{
    // Giả sử tải mất thời gian (mô phỏng bằng timer)
    Task.Delay(1000).ContinueWith(_ =>
    {
        string data = "Kết quả tìm kiếm: <dữ liệu>";
        callback(data); // Gọi delegate callback
    });
}

// Sử dụng:
DownloadDataAsync(result =>
{
    Console.WriteLine("Đã nhận: " + result);
});

Cách tiếp cận này giúp viết code rất linh hoạt, logic sau khi nhận kết quả hoàn toàn tách biệt khỏi cơ chế lấy dữ liệu.

2. Delegate như tham số của method: strategy và comparator

Một nhiệm vụ phổ biến: cho phép người dùng truyền "logic" (hàm) vào method của bạn, để họ quyết định cách so sánh, lọc hoặc biến đổi phần tử.

Ví dụ 2: Triển khai pattern Strategy bằng delegate

Giả sử chúng ta có thuật toán sort, nhưng muốn người dùng có thể sắp xếp theo nhiều cách — theo tên, theo ngày, theo kích thước, v.v.


public delegate bool CompareFunc(int a, int b);

public void BubbleSort(int[] arr, CompareFunc compare)
{
    for (int i = 0; i < arr.Length; i++)
    {
        for (int j = 0; j < arr.Length - 1; j++)
        {
            if (compare(arr[j], arr[j + 1]))
            {
                // Hoán đổi
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

// Sắp xếp từ lớn đến nhỏ
CompareFunc descending = (a, b) => a < b;

// Sử dụng
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // Sẽ in: 4, 3, 2, 1

Cách này là cách phổ biến để tiêm "strategy" vào code của người khác mà không cần sửa nguồn.

3. Anonymous methods, lambda expressions và delegate

Khi C# phát triển thì không tiện để tạo class hay method riêng cho mọi tác vụ. May mắn là có anonymous methods và lambda expressions, cho phép tạo delegate "on the fly".

Ví dụ 3: Lambda làm delegate


Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15

Ví dụ 4: Chọn phép toán theo tên (switch + Delegates)


Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"

switch (userInput)
{
    case "sum": op = (a, b) => a + b; break;
    case "sub": op = (a, b) => a - b; break;
    case "mul": op = (a, b) => a * b; break;
    case "div": op = (a, b) => a / b; break;
    default: throw new Exception("Phép toán không xác định!");
}
Console.WriteLine(op(6, 2));

Đừng quên validate input — delegate ở đây mang lại sự linh hoạt và dễ đọc.

4. Delegate và chuỗi xử lý (“chain of responsibility”)

Vì delegate hỗ trợ multicast, bạn có thể dễ tạo chuỗi handler.

Ví dụ 5: Chuỗi filter

Giả sử ta có các "filter" xử lý một chuỗi.


public delegate string StringFilter(string input);

string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();

StringFilter filters = RemoveDigits;
filters += ToUpper;

// Delegate sẽ đưa chuỗi qua tất cả filter
string text = "Xin chào123";
foreach (StringFilter filter in filters.GetInvocationList())
{
    text = filter(text);
}
Console.WriteLine(text); // Sẽ in: "XIN CHÀO"

Lưu ý: nếu gọi đơn giản filters(text), sẽ trả về giá trị chỉ của handler cuối cùng, chứ không phải của cả chuỗi! Nếu cần "chảy" giá trị, iterate bằng GetInvocationList() như ví dụ trên.

5. Delegate để gắn hành vi động khi runtime

Trước đây để thay đổi hành vi thường phải làm class/interface riêng. Với delegate và lambda bạn có thể biểu diễn nhiều phần "polymorphism nhỏ" bằng hàm.

Ví dụ 6: Robot với command động


public class Robot
{
    public event Action<string>? OnCommandReceived;

    public void ReceiveCommand(string command)
    {
        OnCommandReceived?.Invoke(command);
    }
}

// Sử dụng:
var robot = new Robot();

robot.OnCommandReceived += cmd => Console.WriteLine($"Robot thực hiện: {cmd}");
robot.OnCommandReceived += cmd =>
{
    if (cmd == "Bật")
        Console.WriteLine("Đang nạp hệ thống...");
};
// Thử
robot.ReceiveCommand("Bật");
robot.ReceiveCommand("Di chuyển về phía trước");

Cách này hay dùng trong tests, prototypes, DI container và để truyền business logic qua parameters.

6. Delegate như subscription cho trạng thái

Giả sử có lớp giữ một trạng thái nào đó, và khi nó đổi bạn muốn thông báo cho mọi subscriber. Với delegate (và event) điều này rất đơn giản.

Ví dụ 7: Lớp có subscription khi thay đổi


public class Notifier<T>
{
    private T _value = default!;
    public event Action<T>? ValueChanged;

    public T Value
    {
        get => _value;
        set
        {
            if (!Equals(_value, value))
            {
                _value = value;
                ValueChanged?.Invoke(_value);
            }
        }
    }
}

// Sử dụng:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"Giá trị mới: {v}");
intValue.Value = 5; // Kích hoạt event
intValue.Value = 10;

Cách này gần giống "reactive programming tối giản", nền tảng cho MVVM, data binding và nhiều UI framework hiện đại.

7. Delegate, closure và phạm vi lexical

Lambda và anonymous method có thể capture biến từ context xung quanh (closure). Tiện nhưng đôi khi gây lỗi bất ngờ.

Ví dụ 8: Capture biến và "bẫy" trong vòng lặp


Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
{
    actions[i] = () => Console.WriteLine(i);
}

foreach (var a in actions)
    a(); // Sẽ in ba lần 3 (!)

Tại sao? Vì closure tham chiếu tới cùng một biến i, mà sau vòng lặp i = 3. Nếu muốn nhớ giá trị 0,1,2?


for (int i = 0; i < 3; i++)
{
    int loopValue = i; // "đóng băng" giá trị hiện tại
    actions[i] = () => Console.WriteLine(loopValue);
}

Giờ code chạy như mong đợi. Những cái bẫy này là lỗi thường gặp với người mới dùng lambda!

8. Kết hợp nhiều delegate

Multicast delegate chứa danh sách method, bạn có thể thêm (+=) hoặc bỏ (-=) handler.

Đặc điểm: bỏ theo reference và signature


void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");

Action a = Handler1;
a += Handler2;
a -= Handler1; // Chỉ còn Handler2
a?.Invoke();   // Sẽ in "2"

Ví dụ: quản lý handler động


Action a = Handler1;
a += Handler1;
a -= Handler1; // Bây giờ chỉ còn MỘT Handler1 trong danh sách!

9. Delegate, extensibility và inversion of control (IoC)

Trong app lớn, component thường cần "gọi" code bên ngoài mà vẫn giữ độc lập. Delegate giúp đưa extension, plugin và callback vào mà không tạo tight coupling.

Ví dụ: Inject hành vi qua constructor


public class Greeter
{
    private readonly Func<string> _getName;

    public Greeter(Func<string> getName)
    {
        _getName = getName;
    }

    public void Greet() => Console.WriteLine($"Chào, {_getName()}!");
}

// Inject các hành vi khác nhau:
var greeter1 = new Greeter(() => "Anya");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());

greeter1.Greet(); // "Chào, Anya!"
greeter2.Greet(); // "Chào, 14:35!"

Trong thực tế cách này hay dùng để viết code dễ test và maintain.

10. Các mẹo hữu ích

Delegate trong các interface chuẩn và LINQ

Bạn sẽ gặp delegate rất nhiều nếu làm việc với LINQ, collections, bất đồng bộ.

  • Nhiều method như List<T>.Find, Array.Sort, Where, Select nhận delegate (Func<T, bool>, Comparison<T> v.v.).
  • Phương thức LINQ cho phép truyền logic lọc, biến đổi, tổng hợp — mà không cần tạo class riêng.

Ví dụ: Comparator để sort object


var people = new[] { "Ivan", "Maria", "Petr" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // Ivan, Petr, Maria

Delegate và currying (partial application)

Với anonymous method/lambda bạn có thể "khóa" một phần tham số và tạo hàm mới.

Ví dụ: Partial application


Func<int, int, int> sum = (x, y) => x + y;

// Tạo hàm luôn cộng thêm 10
Func<int, int> add10 = y => sum(10, y);

Console.WriteLine(add10(5)); // 15

Đặc điểm so sánh delegate

Trong C# delegate có thể so sánh bằng nhau (==) nếu chúng có cùng invocation list.


void Handler1() { }
void Handler2() { }

Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True

Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True

Nhưng nếu delegate được tạo từ anonymous method hay lambda — thì so sánh phụ thuộc vào instance.

Serializing delegate

Delegate có thể serialize được, nhưng chỉ khi các method mà chúng trỏ tới nằm trong class có thể serialize và mọi type đều khả dụng. Từ .NET 8, BinaryFormatter bị tắt mặc định và được coi là deprecated; trong các ứng dụng production việc serialize delegate hiếm khi dùng.

Tương tác giữa delegate và event: khi nào dùng cái nào?

  • Delegate là một type/variable có thể gọi trực tiếp.
  • Event (event) là cách giới hạn truy cập tới delegate: bên ngoài chỉ được subscribe/unsubscribe (+=/-=), còn invoke chỉ từ trong class.
  • Sự kiện luôn có kiểu delegate, nhưng không phải mọi delegate đều là event.

Khi nào dùng? Nếu muốn logic được xác định "bên ngoài" class thì dùng delegate. Nếu cần kiểm soát đăng ký/hủy đăng ký và bảo vệ biến — dùng event.

11. Lỗi thường gặp khi làm việc với delegate

Lỗi #1: Nhầm lẫn giữa delegate và event.
Dùng public delegate field (public Action MyAction;) thay vì event (public event Action MyAction;) phá vỡ encapsulation. Code bên ngoài có thể ghi đè tất cả subscriber (instance.MyAction = null;) hoặc gọi trực tiếp, làm rối logic class.

Lỗi #2: Xử lý sai giá trị trả về của multicast delegate.
Nếu delegate trả về giá trị (ví dụ Func<string, int>), gọi bình thường (myDelegate("test")) chỉ trả kết quả của method cuối cùng trong chuỗi. Để lấy kết quả của tất cả subscriber, phải iterate qua invocation list bằng GetInvocationList().


// Ví dụ lặp kết quả của tất cả subscriber
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
    var r = ((Func<string, int>)d)("test");
    Console.WriteLine(r);
}

Lỗi #3: Capture biến vòng lặp trong closure.
Bẫy kinh điển cho người mới: lambda tạo trong vòng lặp for capture biến iterator chứ không phải giá trị hiện tại.


// Sai: tất cả action sẽ in giá trị i cuối cùng
for (int i = 0; i < 3; i++) 
{
    actions[i] = () => Console.WriteLine(i);
}

// Đúng: tạo bản sao cục bộ cho mỗi vòng lặp
for (int i = 0; i < 3; i++) 
{
    int copy = i;
    actions[i] = () => Console.WriteLine(copy);
}

Lỗi #4: Gây memory leak.
Nếu method instance subscribe vào delegate của object sống lâu hơn nhưng không unsubscribe, sẽ tạo leak. Object sống lâu giữ reference tới subscriber, GC không thể thu. Chú ý unsubscribe, đặc biệt trong các lớp lifecycle ngắn (ví dụ component UI).

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION