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ểu | Tham số | Trả về |
|---|---|---|
Action | không có | void |
Action<T> | T | void |
Action<T1, T2> ... đến 16 | T1, T2, ... | void |
Func<TResult> | không có | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... đến 16 | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int (âm, bằng không, dương) |
Converter<TIn, TOut> | TIn | TOut |
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,outhoặcin.Funckhông biểu diễn được chúng, nên một hàm kiểuTryParsecầndelegate 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ơnFunc<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);.