Menu

Func và Action C#: delegate generic, Predicate và ví dụ

Cách Func và Action hoạt động trong C#: Func<T, TResult> cho hàm trả về giá trị, Action<T> cho hàm không trả về, Predicate và Comparison, truyền và trả về hàm, từ điển lệnh, lambda async, và khi nào nên tự khai báo delegate riêng.

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.

Func và Action là các kiểu delegate generic được .NET khai báo sẵn cho bạn, nhờ vậy bạn có thể lưu và truyền hàm mà không phải khai báo kiểu delegate trước. Func trả về một giá trị; Action không trả về gì.

Đầu ra:

4
46.00
False
Hello!
Mia scored 87

Quy tắc cần nhớ: trong Func, đối số kiểu cuối cùng là kiểu trả về. Func<int, int, bool> nhận hai int và trả về một bool. Func<int> không nhận gì và trả về một int. Các đối số kiểu của Action đều là tham số.

Họ delegate có sẵn

KiểuTham sốTrả về
Actionkhông cóvoid
Action<T>Tvoid
Action<T1, T2> ... đến 16T1, T2, ...void
Func<TResult>không cóTResult
Func<T, TResult>TTResult
Func<T1, T2, TResult> ... đến 16T1, T2, ...TResult
Predicate<T>Tbool
Comparison<T>T, Tint (âm, bằng không, dương)
Converter<TIn, TOut>TInTOut

Predicate, Comparison và Converter ra đời sớm hơn và xuất hiện trong các phương thức của List<T> và Array: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ thì dùng Func ở mọi nơi, vì vậy list.Where(...) và list.FindAll(...) nhận lambda cùng dạng nhưng khác kiểu:

Đầu ra:

17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y

Truyền hàm vào phương thức

Một tham số Func hoặc Action cho phép nơi gọi cung cấp phần thay đổi của một thuật toán. Hai dạng hay gặp là "chạy cái này và đo thời gian" và "thử cái này đến khi thành công":

Đầu ra:

attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2

Retry là generic, nên Func<int, T> của nó trả về được bất cứ thứ gì; trình biên dịch suy ra T là string từ lambda. Lambda truyền vào Twice thay đổi biến cục bộ clicks, điều này được vì lambda bắt (capture) chính biến đó chứ không phải một bản sao giá trị của nó.

Method group: truyền phương thức bằng tên

Bạn không cần lambda để gán cho Func hay Action. Tên của một phương thức có sẵn với chữ ký khớp (gọi là "method group") được chuyển đổi trực tiếp:

Đầu ra:

parsed: 43
sat is weekend: True
total quantity: 20

Select(int.Parse) tương đương Select(s => int.Parse(s)), chỉ bớt đi một lớp lời gọi. Trình biên dịch chọn overload khớp với kiểu đích: int.Parse có nhiều overload, và chỉ Parse(string) khớp với Func<string, int>. Khi trình biên dịch không chốt được một overload (nhiều overload khớp ngang nhau, hoặc không suy ra được đối số kiểu của một phương thức generic từ method group), lời gọi sẽ không biên dịch, và một lambda viết rõ lời gọi sẽ khắc phục được.

Một method group dùng làm Func vẫn chạy về sau, mỗi khi delegate được gọi, giống hệt một lambda. Khác biệt chỉ nằm ở cách viết.

Trả về hàm

Một phương thức có thể tạo ra và trả về một hàm. Func được trả về mang theo các giá trị đã dùng để tạo ra nó:

Đầu ra:

140
180
162

Twice(staff) tạo một hàm mới áp dụng giảm giá cho nhân viên hai lần: 200, rồi 180, rồi 162.

Từ điển chứa hàm

Lưu hàm trong một từ điển biến một switch dài thành một bảng tra cứu. Đây là dạng thường dùng cho trình xử lý lệnh, máy tính và các hành động trong menu, và có thể thêm mục mới lúc chương trình đang chạy:

Đầu ra:

12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE

Thêm một toán tử chỉ là thêm một mục vào từ điển, không phải sửa vòng lặp. StringComparer.OrdinalIgnoreCase truyền vào từ điển thứ hai giúp tên lệnh không phân biệt hoa thường.

Hàm async: Func<Task>, không phải Action

Với code async, kiểu delegate quyết định nơi gọi có chờ được công việc hay không:

Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save();                       // the caller can await it and see its exceptions

Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);

Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad();                              // async void: fire and forget, exceptions escape

Một lambda async gán cho Action sẽ thành một phương thức async void: không gì await được nó, và ngoại lệ bên trong không bao giờ đến được nơi gọi. Ngoại lệ đó bị ném lại trên thread pool (hoặc trên luồng giao diện trong ứng dụng desktop), nơi nó thường làm sập tiến trình. Khi viết một phương thức nhận công việc async, hãy nhận Func<Task> (hoặc Func<Task<T>>).

Khi nào nên tự khai báo delegate

Func và Action đáp ứng phần lớn nhu cầu. Hãy khai báo một kiểu delegate có tên khi:

  • Chữ ký có tham số ref, out hoặc in. Func không biểu diễn được chúng, nên một hàm kiểu TryParse cần delegate bool TryParser<T>(string text, out T value);.
  • Cái tên ghi lại vai trò trong một API public. delegate bool Validator(string input) trong chữ ký phương thức nói rõ hơn Func<string, bool>, và tên tham số của nó hiện ra trong IntelliSense.
  • Bạn cần một tham số params, điều mà delegate generic cũng không làm được.

Event là nơi còn lại vẫn thấy kiểu delegate tự định nghĩa, dù EventHandler<TEventArgs> có sẵn đã đáp ứng phần lớn trường hợp. Trang delegate trình bày cách khai báo và kết hợp các kiểu delegate.

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

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

Func trả về một giá trị còn Action thì không. Trong Func<int, string, bool>, đối số kiểu cuối cùng (bool) là kiểu trả về và các đối số còn lại là tham số. Action<int, string> nhận một int và một string rồi trả về void. Cả hai đều có các phiên bản từ 0 đến 16 tham số.

Làm sao truyền một hàm làm tham số trong C#?

Khai báo tham số là một Func hoặc Action có dạng phù hợp và gọi nó bên trong phương thức: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. Nơi gọi truyền vào một lambda (Apply(80m, p => p * 0.9m)) hoặc tên một phương thức (Apply(80m, HalfPrice)).

Predicate<T> trong C# là gì?

Là một kiểu delegate nhận một T và trả về bool, cùng dạng với Func<T, bool>. Nó ra đời trước Func và được dùng bởi List<T>.Find, FindAll, RemoveAll, Exists và Array.Find. Hai kiểu này không chuyển đổi qua lại được, nhưng một lambda chuyển được sang kiểu nào cũng được.

Dùng Func với code async như thế nào?

Dùng Func<Task> cho hàm async không có kết quả và Func<T, Task<TResult>> cho hàm có kết quả, để nơi gọi có thể await. Tránh gán một lambda async cho Action: làm vậy biến nó thành async void, không thể await, và nơi gọi không bắt được ngoại lệ của nó.

Func có tham số out hoặc ref được không?

Không. Các tham số generic của Func và Action là tham số truyền giá trị thông thường, nên không có cách nào viết Func<string, out int, bool>. Với chữ ký kiểu TryParse, hãy khai báo một delegate riêng: delegate bool TryParser<T>(string text, out T value);.

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