1. Giới thiệu
Bạn biết điều thú vị không? Biểu thức lambda tự nó — giống như công thức mà không có nồi. Nó mô tả phải làm gì, nhưng cần một "khung" để sống trong code. Trong C# có sẵn các delegate tổng quát để làm điều đó: Func, Action và Predicate.
Hãy tưởng tượng chúng như những khuôn sẵn cho lambda của bạn — chọn khuôn rồi đổ logic vào. Không cần tự tạo delegate riêng khi đã có sẵn mọi thứ cần thiết.
Một ít lịch sử
Ngày trước, khi cần truyền một hàm làm tham số, phải khai báo delegate riêng. Việc đó lâu và rườm rà, trông kiểu như:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
Khi C# có Func, Action và Predicate, bạn có thể quên việc khai báo delegate thủ công trong 90% trường hợp. Bây giờ trông đơn giản và tổng quát hơn nhiều:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — hàm có trả về
Cú pháp và mục đích
Func — là delegate generic, nhận từ 0 đến 16 tham số và trả về một giá trị.
Func<int, int, int> sum = (x, y) => x + y;
Func<kiểu1, kiểu2, ..., kiểuN, TResult> — tất cả các tham số trước cái cuối cùng là arguments, cái cuối cùng là kiểu trả về.
Ví dụ
1. Tổng hai số
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. Lấy bình phương một số
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. Không có tham số
Func<string> greet = () => "Privet, lambda!";
Console.WriteLine(greet());
Sơ đồ trực quan
| Chữ ký | Ví dụ | Mô tả |
|---|---|---|
|
|
Nhận int, trả về int |
|
|
Hai int, trả về int |
|
|
Không nhận gì, trả về chuỗi |
Trông nó như thế nào trong ứng dụng của bạn
Giả sử trong mini-app của chúng ta (gọi là "Danh bạ người dùng"), có một list số — muốn áp dụng các xử lý khác nhau. Ví dụ, lấy bình phương hoặc tính tổng với một số cố định:
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25
3. Action<T> — hành động không trả về
Action — delegate tổng quát cho các phương thức làm gì đó (ví dụ, in ra màn hình), nhưng không trả về gì.
Có thể nhận từ 0 đến 16 tham số, nhưng luôn trả về void.
Ví dụ
1. In ra màn hình
Action<string> print = text => Console.WriteLine("Dannye: " + text);
print("Privet, mir!");
2. Hành động không tham số
Action greet = () => Console.WriteLine("Dobro pozhalovat!");
greet();
3. Hành động với nhiều tham số
Action<int, int> showSum = (a, b) => Console.WriteLine($"Summa: {a + b}");
showSum(2, 3); // Summa: 5
Sơ đồ trực quan
| Chữ ký | Ví dụ | Mô tả |
|---|---|---|
|
|
Không tham số, không trả về |
|
|
Một tham số |
|
|
Nhiều tham số |
Trong ứng dụng của chúng ta
Thêm vào danh bạ người dùng một method để in tất cả tên:
List<string> names = new() { "Anna", "Boris", "Vika" };
Action<string> printName = name => Console.WriteLine("Polzovatel': " + name);
names.ForEach(printName);
// hoặc thế này: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — có hay không?
Khi cần một hàm trả về chỉ true hoặc false cho một tham số, dùng Predicate<T>. Đây đơn giản là delegate nhận một tham số và trả về bool.
Predicate — chính thức là "cái để kiểm tra boolean" bọc cho lambda.
Ví dụ
1. Kiểm tra số có lớn hơn 5 không
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. Dùng với phương thức List<T>.Find
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // dùng Predicate<int>
Console.WriteLine(found); // 7
3. Tất cả đã trưởng thành chưa?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll nhận Predicate<int>
Khác biệt với Func<T, bool> ở chỗ nào?
Về thực tế thì chúng có thể thay thế cho nhau. Ngay cả tài liệu Microsoft cũng nói: "Predicate<T> — chỉ là Func<T, bool> cho các API đặc thù". Nhưng một số method trong thư viện chuẩn vẫn yêu cầu chính xác Predicate.
5. Cách lambda "khớp" với Func, Action, Predicate
Khi bạn viết lambda, C# phân tích: "Ồ, hình dạng của nó phù hợp delegate yêu cầu — có thể gán!"
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
Ở đâu đâu cũng lambda! Nhưng bên dưới — là ba delegate khác nhau với chữ ký khác nhau.
Áp dụng trong mã "thực"
List<User> users = new() {
new User("Anna", 24),
new User("Boris", 17),
new User("Vika", 31),
};
// Hàm trả về chỉ những người đủ 18 tuổi (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("Spisok vzroslykh: " + string.Join(", ", adults.Select(u => u.Name)));
Nếu muốn in tên tất cả người dùng bằng Action<User>:
users.ForEach(user => Console.WriteLine(user.Name));
Lấy tên của họ (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
Bảng để dễ nhìn
| Delegate | Chữ ký | Ví dụ lambda | Nơi dùng |
|---|---|---|---|
|
T → U | |
Select, mọi chuyển đổi |
|
T → void | |
ForEach, các phương thức hành động |
|
T → bool | |
Find, Exists, các bộ lọc |
6. Ví dụ với ứng dụng nội bộ: từng bước
Mình mở rộng nhỏ ứng dụng "Danh bạ người dùng". Giả sử chúng ta có class User:
public class User
{
public string Name { get; }
public int Age { get; }
public bool IsActive { get; set; }
public User(string name, int age)
{
Name = name;
Age = age;
IsActive = true;
}
}
1. Func<User, bool> — kiểm tra user đã đủ tuổi chưa
Func<User, bool> isAdult = user => user.Age >= 18;
Dùng trong LINQ:
var adults = users.Where(isAdult);
2. Predicate<User> — tìm user đang active
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — deactivate user
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — lấy mô tả ngắn
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
Tất cả những lambda này — là "code dưới dạng data", có thể truyền vào method, lưu trong biến, kết hợp với nhau.
7. Những điểm không rõ ràng và lỗi thường gặp
1. Lambda phải phù hợp chữ ký của delegate. Nếu chữ ký không khớp sẽ lỗi biên dịch.
Func<int, string> wrong = x => x * 2; // lỗi: mong đợi string, nhận int
// Đúng:
Func<int, string> right = x => (x * 2).ToString();
2. Đừng quên về void vs return. Action không trả về giá trị — cố gắng viết kiểu Action<int> a = x => x * x; sẽ không được vì có giá trị trả về trong khi không nên có.
3. Predicate<T> và Func<T, bool> thường có thể thay thế nhau, nhưng không phải lúc nào. Đôi khi các method collection yêu cầu chính xác Predicate<T>, đôi khi yêu cầu Func<T, bool>. Gán trực tiếp đôi khi không chạy được nếu không bọc rõ ràng.
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // lỗi
// Nhưng:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // có thể chuyển qua constructor
GO TO FULL VERSION