Menu

Delegate trong C#: khai báo, gọi, multicast và callback

Delegate trong C# là gì và cách dùng: khai báo kiểu delegate, tạo delegate từ phương thức, lambda hoặc phương thức ẩn danh, gọi delegate, delegate làm callback, multicast delegate với += và -=, kiểm tra null bằng ?.Invoke, và quan hệ giữa delegate với Func, Action và event.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Delegate là một kiểu đại diện cho chữ ký phương thức. Biến có kiểu delegate giữ tham chiếu đến một phương thức có chữ ký đó, và gọi biến chính là gọi phương thức. Đây là cách C# coi hàm như giá trị: bạn có thể lưu chúng, truyền chúng cho phương thức khác và gọi chúng sau.

Đầu ra:

40
70
68.00

delegate decimal PriceRule(decimal price); khai báo một kiểu mới, giống như class. Mọi phương thức nhận một decimal và trả về một decimal đều khớp với nó. Biến rule lần lượt trỏ đến ba phương thức khác nhau, và mỗi lần gọi chạy phương thức mà nó đang giữ.

Khai báo, tạo và gọi delegate

Khai báo kiểu delegate trông giống chữ ký phương thức có từ khóa delegate ở đầu. Nó thường nằm ở cấp namespace, cạnh các lớp, hoặc lồng bên trong một lớp.

Có bốn cách tạo một instance delegate:

PriceRule a = HalfPrice;                             // method group (most common)
PriceRule b = new PriceRule(HalfPrice);              // explicit constructor, same thing
PriceRule c = p => p - 5m;                            // lambda expression
PriceRule d = delegate (decimal p) { return p; };    // anonymous method (C# 2 syntax)

Và hai cách gọi nó, hoàn toàn giống nhau: rule(80m) và rule.Invoke(80m). Trình biên dịch kiểm tra chữ ký lúc gán, nên một phương thức không khớp là lỗi biên dịch chứ không phải bất ngờ lúc chạy.

Delegate cũng có thể trỏ đến phương thức của instance. Khi đó nó nhớ cả phương thức lẫn đối tượng sẽ gọi phương thức đó:

Đầu ra:

Ana got: Your order shipped
target: Mailbox, method: Receive

Target là đối tượng mà delegate sẽ gọi phương thức trên đó (null với phương thức tĩnh), còn Method mô tả phương thức. Việc giữ target cũng có nghĩa là delegate giữ đối tượng đó sống, điều này quan trọng với event.

Delegate làm callback

Nhiệm vụ chính của delegate là cho một phương thức gọi code mà nơi gọi nó đã chọn. Phương thức quyết định khi nào điều gì đó xảy ra; nơi gọi quyết định điều gì xảy ra:

Đầu ra:

[##        ] 25%
[#####     ] 50%
[#######   ] 75%
[##########] 100%
import finished

ImportRows không biết gì về console hay thanh tiến độ. Cùng một phương thức có thể điều khiển thanh tiến độ, một dòng log hay cập nhật UI, tùy vào thứ nơi gọi truyền vào. List.Sort(Comparison<T>), Array.Find(Predicate<T>), Task.Run(Action) và mọi toán tử LINQ đều theo mẫu này.

Multicast delegate: += và -=

Một delegate có thể giữ nhiều phương thức. += nối thêm một phương thức vào danh sách gọi, -= gỡ nó ra, và gọi delegate sẽ gọi từng phương thức theo thứ tự:

Đầu ra:

email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002

Ba chi tiết của multicast delegate quan trọng trong thực tế:

  • Giá trị trả về: nếu kiểu delegate trả về giá trị, gọi một multicast delegate chỉ trả về kết quả của phương thức cuối cùng. Để lấy mọi kết quả, hãy duyệt GetInvocationList() và tự gọi từng phương thức.
  • Ngoại lệ: nếu một phương thức ném ngoại lệ, các phương thức còn lại không chạy và ngoại lệ đi đến nơi gọi.
  • Tính bất biến: delegate là bất biến. += tạo một delegate mới với danh sách dài hơn rồi gán lại cho biến, đó là lý do gỡ bằng -= trên một bản sao không ảnh hưởng đến bản gốc.

Gỡ một lambda bằng -= chỉ có tác dụng nếu bạn truyền cùng một instance delegate. Viết cùng một lambda hai lần tạo ra hai delegate khác nhau, nên handlers -= id => Log(id); không gỡ được gì. Hãy lưu lambda vào một biến khi bạn định hủy đăng ký nó.

Delegate null và ?.Invoke

Biến delegate chưa được gán gì là null, và gỡ phương thức cuối cùng bằng -= cũng để lại null. Gọi null sẽ ném NullReferenceException:

Đầu ra:

no handler, no crash
saved draft.txt
NullReferenceException

?.Invoke là cách chuẩn để gọi một callback tùy chọn. Toán tử ?. không đặt ngay trước dấu ngoặc đơn được (onSaved?("x") là cú pháp không hợp lệ), nên mẫu này gọi phương thức Invoke của delegate theo tên, có tác dụng giống hệt cách gọi viết tắt.

Delegate, Func, Action và event

Khai báo một kiểu delegate cho mỗi chữ ký thì rất mất công, nên .NET có sẵn các kiểu generic:

Kiểu có sẵnKhai báo tùy chỉnh tương đương
Actiondelegate void Action();
Action<T>delegate void Action<T>(T arg);
Func<TResult>delegate TResult Func<TResult>();
Func<T, TResult>delegate TResult Func<T, TResult>(T arg);
Predicate<T>delegate bool Predicate<T>(T obj);

PriceRule ở ví dụ đầu có cùng hình dạng với Func<decimal, decimal>, nhưng hai kiểu này khác nhau và không chuyển đổi qua lại được. Ngay cả hai kiểu có sẵn với chữ ký giống hệt nhau cũng không:

Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'

Predicate<int> ok = new Predicate<int>(isPositive);   // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x);              // or with a lambda

Lambda chuyển đổi được sang mọi kiểu delegate tương thích, nên API thường nhận lambda và câu hỏi này không bao giờ xuất hiện. Trang Func và Action nói về họ kiểu generic này và khi nào khai báo tùy chỉnh vẫn đáng dùng.

Event được xây dựng trực tiếp trên delegate. Một field event là multicast delegate mà code bên ngoài chỉ được += và -=, không bao giờ được gọi hay ghi đè. Danh sách subscriber trong ví dụ multicast ở trên chính là thứ event quản lý giúp bạn, kèm thêm lớp bảo vệ đó. Event được trình bày ở một trang riêng.

Câu hỏi thường gặp

Delegate trong C# là gì?

Delegate là một tham chiếu an toàn về kiểu đến một phương thức. Bạn khai báo kiểu delegate với một chữ ký, chẳng hạn delegate decimal PriceRule(decimal price);, và mọi phương thức có chữ ký đó (tĩnh, của instance, hoặc lambda) đều có thể được lưu vào biến kiểu này rồi được gọi sau thông qua biến. Delegate là cách C# truyền hàm như một giá trị.

Delegate và Func trong C# khác nhau thế nào?

Func<T, TResult> và Action<T> là các kiểu delegate mà .NET đã khai báo sẵn cho bạn, generic theo kiểu tham số và kiểu trả về. Một khai báo delegate tùy chỉnh đặt tên cho kiểu và tên tham số để mô tả vai trò của nó, đồng thời hỗ trợ tham số ref, out và in. Với phần lớn code, Func và Action là đủ.

Multicast delegate trong C# là gì?

Là delegate chứa nhiều phương thức. += thêm một phương thức vào danh sách gọi của nó và -= gỡ một phương thức ra; gọi delegate sẽ gọi tất cả theo thứ tự. Nếu delegate trả về giá trị, nơi gọi chỉ nhận được kết quả của phương thức cuối cùng, và một ngoại lệ trong một phương thức sẽ chặn các phương thức còn lại.

Làm sao gọi an toàn một delegate có thể là null?

Dùng toán tử điều kiện null: onProgress?.Invoke(50);. Biến delegate chưa được gán phương thức nào là null, và gọi trực tiếp nó sẽ ném NullReferenceException. ?.Invoke chỉ đọc biến một lần, nên vẫn an toàn nếu một luồng khác gỡ handler cuối cùng ngay lúc đó.

Khi nào nên tự khai báo kiểu delegate thay vì dùng Func hoặc Action?

Khi chữ ký cần tham số ref, out hoặc in (Func và Action không biểu diễn được), khi một kiểu có tên làm API công khai rõ ràng hơn (delegate bool Validator(string input)), hoặc khi bạn muốn tên tham số hiện ra trong IntelliSense. Còn lại, hãy ưu tiên Func và Action, những kiểu mà lập trình viên .NET nào cũng nhận ra.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu