CodeGym /Các khóa học /C# SELF /Truyền biểu thức lambda ( =>) như tham số

Truyền biểu thức lambda ( =>) như tham số

C# SELF
Mức độ, Bài học
Có sẵn

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.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Lọc và chuyển đổi người dùng phức tạp
Lọc và chuyển đổi người dùng phức tạp
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION