1. Giới thiệu
Cho đến giờ bạn đã thấy: chúng ta gọi các phương thức LINQ mà truyền vào lambda. Nhưng làm sao một phương thức C# biết là nó có thể nhận một hàm?
Nói ngắn gọn, delegate giống như một interface cho hàm: ta mô tả chữ ký (kiểu tham số và kiểu trả về), và bất kỳ phương thức nào (hay lambda!) phù hợp chữ ký đó có thể được truyền đến nơi đang chờ delegate đó.
Hãy nhớ cách bạn truyền một chuỗi như tham số? Về cơ bản “logic” cũng được truyền tương tự, chỉ khác là kiểu tham số là một delegate.
Delegates: lý thuyết cơ bản nói dễ hiểu
Trong C# delegate là một kiểu mô tả “hàm có chữ ký như thế này”.
// Delegate nhận int và trả về bool
public delegate bool IntPredicate(int x);
Bất kỳ hàm nào tương thích theo chữ ký đều có thể được gán cho biến kiểu này:
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
Và giờ — lambda cũng hợp:
IntPredicate pred = x => x % 2 == 0;
Delegates generic: Func, Action, Predicate
- Func<T1, ..., TResult> — một hàm nhận các tham số T1, ... và trả về TResult.
- Action<T1, ...> — một hàm nhận tham số và không trả về giá trị (void).
- Predicate<T> — một hàm nhận T và trả về bool.
2. Truyền lambda vào phương thức của bạn
Giả sử chúng ta phát triển một mini-app học tập — project console làm việc với danh sách người dùng. Trước đây ta lọc collection qua LINQ, giờ ta viết phương thức của riêng mình nhận điều kiện dưới dạng lambda.
Tạo phương thức nhận lambda làm tham số
// Định nghĩa lớp User để ví dụ (thêm vào ứng dụng của chúng ta)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// Phương thức nhận danh sách và delegate-điều kiện (lambda)
public static List<User> FilterUsers(List<User> users, Predicate<User> predicate)
{
var result = new List<User>();
foreach (var user in users)
{
if (predicate(user)) // Gọi lambda!
result.Add(user);
}
return result;
}
Bây giờ có thể truyền bất kỳ lambda nào:
var users = new List<User>
{
new User { Name = "Vasya", IsActive = true },
new User { Name = "Petya", IsActive = false },
new User { Name = "Masha", IsActive = true }
};
// Lọc chỉ những người dùng đang hoạt động
var activeUsers = FilterUsers(users, user => user.IsActive);
foreach (var user in activeUsers)
Console.WriteLine(user.Name); // Vasya, Masha
Thế thôi! Ta đã truyền một mẩu logic — một mini-hàm — như một tham số bình thường, vì phương thức FilterUsers chờ Predicate<User>, và ta đưa cho nó một lambda phù hợp.
Phiên bản với Func<T, TResult>
Predicate<T> hợp khi cần một điều kiện (trả về bool). Còn nếu muốn “tính toán” giá trị cho mỗi user thì sao?
// Phương thức áp dụng một hàm cho mỗi phần tử và gom kết quả lại
public static List<TResult> MapUsers<TResult>(List<User> users, Func<User, TResult> selector)
{
var result = new List<TResult>();
foreach (var user in users)
{
result.Add(selector(user));
}
return result;
}
Sử dụng:
var names = MapUsers(users, user => user.Name.ToUpper());
foreach (var name in names)
Console.WriteLine(name); // VASYA, PETYA, MASHA
3. Những lưu ý hữu ích
Các dạng truyền khác nhau
Bạn có thể truyền không chỉ lambda mà còn phương thức bình thường — quan trọng là chữ ký phải khớp.
// Hàm bình thường
static bool NameHasS(User user) => user.Name.Contains("s");
// Truyền hàm bình thường:
var usersWithS = FilterUsers(users, NameHasS);
// Truyền lambda
var usersWithA = FilterUsers(users, u => u.Name.Contains("a"));
Cũng có thể dùng anonymous method kiểu cũ (nhưng không cần thiết):
var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("m"); });
Phong cách hiện đại — dùng lambda!
Truyền lambda vào LINQ: thực tế đang xảy ra là gì
var result = users.Where(u => u.IsActive).ToList();
Bên trong Where nhận Func<User, bool>. Điều đó nghĩa là bất kỳ phương thức nào nhận Func<...> cũng có thể dùng tương tự!
Nếu muốn hai tham số thì sao?
// Phương thức nhận hai lambda để lọc
public static List<User> FilterUsersCustom(
List<User> users,
Func<User, bool> include,
Func<User, bool> exclude)
{
var result = new List<User>();
foreach (var user in users)
{
if (include(user) && !exclude(user))
result.Add(user);
}
return result;
}
Sử dụng:
var customFiltered = FilterUsersCustom(
users,
u => u.Name.StartsWith("V"),
u => u.IsActive == false
);
// Sẽ lấy chỉ những user tên bắt đầu bằng "V" và không active
Kịch bản: Nhà máy bộ lọc
Console.WriteLine("Nhập độ dài tối thiểu của tên:");
int minLength = int.Parse(Console.ReadLine());
Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);
// Rất tương tác và sôi nổi!
4. Lỗi thường gặp và lưu ý
Đôi khi compiler không thể “suy ra” kiểu tham số của lambda — đặc biệt trong các trường hợp phức tạp với overload hoặc khi phương thức đòi delegate có nhiều tham số/kiểu trả về cụ thể. Khi đó bạn có thể chỉ định kiểu cho lambda một cách rõ ràng:
FilterUsers(users, (User u) => u.Name.Length > 3);
hoặc thậm chí:
MapUsers(users, (User u) => u.Name.ToUpper());
Lỗi: lambda không hợp chữ ký
FilterUsers(users, user => Console.WriteLine(user.Name)); // lỗi! Mong đợi bool, nhận được void
Vì phương thức mong một hàm trả về bool, còn lambda trên trả void (thực ra không trả gì). Chú ý kiểu trả về!
Lỗi: lạm dụng lambda
Nếu bạn bắt đầu truyền lambda 10 dòng, tốt hơn là tách nó ra thành một phương thức riêng. Vừa dễ đọc, vừa dễ gỡ lỗi.
GO TO FULL VERSION