CodeGym /Các khóa học /C# SELF /Biểu thức lambda và delegate để so sánh

Biểu thức lambda và delegate để so sánh

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

1. Giới thiệu

Hãy tưởng tượng bạn có một danh sách sinh viên, và bạn cần sắp xếp họ một lần theo tuổi, sau đó theo họ, rồi theo điểm trung bình, nhưng chỉ với những ai đã qua tất cả kỳ thi. Nếu cho mỗi kiểu so sánh "một lần" như vậy mà bạn phải viết cả một class mới implement IComparer<T>, thì project của bạn sẽ nhanh chóng thành bãi rác các class-comparator nhỏ lẻ. Rất bất tiện: code sẽ cồng kềnh và khó đọc.

Trong trường hợp như vậy, C# có giải pháp gọn hơn: truyền logic so sánh trực tiếp vào method Sort mà không cần tạo class riêng.

Để làm được thế, ta cần delegate và "người anh em nhỏ gọn" của nó – biểu thức lambda.

Delegate – trợ thủ linh hoạt của chúng ta

Trước khi vào lambda, hãy xem delegate là gì. Nói đơn giản, delegate là kiểu dữ liệu đại diện cho một tham chiếu đến method. Nghe hơi trừu tượng nhỉ? Hãy nghĩ thế này:

Bạn có một danh sách việc cần làm, và một số việc là "hướng dẫn" hoặc "công thức". Delegate giống như một biến đặc biệt có thể lưu tham chiếu đến "công thức" (method) đó. Khi cần thực hiện việc đó, bạn chỉ cần gọi biến-delegate, nó sẽ "gọi" method mà nó tham chiếu.

Trong C#, delegate dùng để tạo callback (gọi method sau, thường là phản ứng với sự kiện), xử lý sự kiện (phản ứng với hành động, ví dụ bấm nút) và tất nhiên, truyền method làm tham số cho method khác (để method này có thể gọi method khác), chính là thứ ta cần cho sort.

Method List<T>.Sort() có nhiều overload (phiên bản), một trong số đó nhận một delegate đặc biệt tên là Comparison<T>.

2. Delegate Comparison<T>

Comparison<T> là gì?

Comparison<T> là delegate có sẵn trong .NET, thiết kế riêng để so sánh hai object cùng kiểu T. "Công thức" của nó như sau: nhận vào hai object kiểu T (gọi là xy) và trả về một số nguyên (int):

  • Số âm (ví dụ -1), nếu x "nhỏ hơn" y.
  • Không (0), nếu x "bằng" y.
  • Số dương (ví dụ 1), nếu x "lớn hơn" y.

Chính theo quy tắc này mà IComparable.CompareToIComparer.Compare cũng hoạt động. Tức là, logic giống nhau, chỉ khác là giờ ta truyền nó như "biến-method", không phải class riêng.

Xem ví dụ nhé. Quay lại với sinh viên. Giả sử ta có class Student:

public class Student
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public double AverageGrade { get; set; }

    public Student(string firstName, string lastName, int age, double averageGrade)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
        AverageGrade = averageGrade;
    }

    public void PrintInfo()
    {
        Console.WriteLine($"Sinh viên: {FirstName} {LastName}, Tuổi: {Age}, Điểm: {AverageGrade:F2}");
    }
}

Giờ để sort danh sách sinh viên theo tuổi dùng delegate, ta có thể viết một method static riêng, đúng signature của Comparison<Student>:

public class Program
{
    // Method đúng signature delegate Comparison<Student>
    // So sánh hai sinh viên theo tuổi
    public static int CompareStudentsByAge(Student student1, Student student2)
    {
        // Dùng method CompareTo có sẵn cho số,
        // trả về -1, 0 hoặc 1 tùy kết quả so sánh.
        return student1.Age.CompareTo(student2.Age);
    }

    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("Ivan", "Petrov", 20, 4.5),
            new Student("Maria", "Sidorova", 22, 4.8),
            new Student("Aleksei", "Ivanov", 19, 3.9),
            new Student("Elena", "Kozlova", 20, 4.2) // Hai sinh viên cùng tuổi
        };

        Console.WriteLine("--- Danh sách sinh viên trước khi sắp xếp ---");        
        foreach (var s in students)
            s.PrintInfo();

        Console.WriteLine("--- Sắp xếp sinh viên theo tuổi (dùng delegate) ---");
        students.Sort(CompareStudentsByAge); //truyền method CompareStudentsByAge làm tham số

        foreach (var s in students)
            s.PrintInfo();
    }
}

Giải thích code:

  1. Ta tạo method static CompareStudentsByAge, nhận hai sinh viên và trả về int, đúng contract Comparison<Student>.
  2. Trong Main ta tạo danh sách sinh viên.
  3. Khi gọi students.Sort(CompareStudentsByAge);, ta không gọi method CompareStudentsByAge() ngay! Chỉ truyền tham chiếu đến method này. List<T>.Sort() sẽ tự gọi method CompareStudentsByAge nhiều lần khi cần sort, truyền cho nó các cặp sinh viên khác nhau. Nó giống như bạn đưa địa chỉ giao hàng, chứ không phải gửi cả xe tải đến đó ngay lập tức.

Cách này tiện hơn nhiều so với việc tạo class-comparator riêng cho mỗi kiểu sort nhỏ. Nhưng ta còn có thể làm gọn hơn nữa!

3. Làm quen với Biểu thức Lambda

Ngay cả việc phải viết method riêng như CompareStudentsByAge cũng có thể thấy thừa nếu logic so sánh đơn giản và chỉ dùng một hai lần. Với trường hợp như vậy, C# có biểu thức lambda (lambda expressions).

Biểu thức lambda là gì? Thực chất, nó là method ẩn danh hoặc, như mình hay đùa, "method vô gia cư". Nó là cách viết nhanh một đoạn code (method) ngay tại chỗ cần dùng, không phải khai báo riêng. Giống như ghi nhanh hướng dẫn lên giấy note và dán ngay vào việc, thay vì viết cả quyển hướng dẫn.

Toán tử chính của lambda là => (đọc là "mũi tên" hoặc "chuyển thành"). Nó tách tham số đầu vào với thân method.

Cú pháp cơ bản của lambda

Giả sử bạn có delegate (tham chiếu method) và truyền nó vào function Sort():

public static int CompareStudentsByAge(Student student1, Student student2)
{
   return student1.Age.CompareTo(student2.Age);
}

students.Sort(CompareStudentsByAge); //truyền method CompareStudentsByAge làm tham số

Có cách viết ngắn hơn:

//truyền method ẩn danh làm tham số
students.Sort( (Student student1, Student student2) => student1.Age.CompareTo(student2.Age) );

Ở đây, thay vì tên method, ta ghi hai thứ quan trọng nhất:

  • tham số: (Student student1, Student student2)
  • nội dung method: student1.Age.CompareTo(student2.Age)

Cách viết gọn này gọi là biểu thức lambda: (tham số) => biểu thức

Nó hoạt động thế nào

Khi gặp lambda, compiler C# sẽ tự sinh ra method thật từ đó.

Ví dụ bạn có code:

students.Sort( (s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade) );

Kết quả biên dịch sẽ kiểu như:

public static int CompareStudents_Lambda123(Student s1, Student s2)
{
   return s2.AverageGrade.CompareTo(s1.AverageGrade);
}

students.Sort( CompareStudents_Lambda123 );

4. Ví dụ sort và lambda

Hãy viết lại ví dụ sinh viên, dùng lambda:

public class Program
{
    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("Ivan", "Petrov", 20, 4.5),
            new Student("Maria", "Sidorova", 22, 4.8),
            new Student("Aleksei", "Ivanov", 19, 3.9),
            new Student("Elena", "Kozlova", 20, 4.2)
        };

        Console.WriteLine("--- Danh sách sinh viên trước khi sắp xếp ---");        
        foreach (var s in students)
            s.PrintInfo();

        // Logic so sánh viết ngay tại đây, "tại chỗ"
        Console.WriteLine("--- Sắp xếp sinh viên theo tuổi (dùng lambda) ---");
        students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));

        foreach (var s in students)
            s.PrintInfo();

        // Để sort giảm dần, chỉ cần nhân kết quả với -1
        Console.WriteLine("\n--- Sắp xếp sinh viên theo điểm trung bình (giảm dần) ---");
        // s2.CompareTo(s1) thay vì s1.CompareTo(s2)
        students.Sort((s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade));

        foreach (var s in students)
            s.PrintInfo();
    }
}

Ở đây có gì?

  1. students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));
    • student1student2 – là tham số mà Sort sẽ truyền cho method ẩn danh của ta (giống xy trong Comparison<T>).
    • => – là toán tử lambda.
    • student1.Age.CompareTo(student2.Age) – là thân lambda. Ở đây chỉ có một biểu thức, kết quả của nó là giá trị trả về.
  2. Để sort theo điểm trung bình giảm dần chỉ cần đảo vị trí s1s2 trong CompareTo. Đây là mẹo kinh điển để đảo ngược thứ tự sort.

Tại sao tiện?

  • Gọn: Không cần tạo method hay class riêng cho mỗi logic so sánh nhỏ.
  • Dễ đọc: Logic so sánh nằm ngay cạnh chỗ gọi Sort(), rất dễ hiểu, nhất là với case đơn giản.
  • Linh hoạt: Dễ đổi điều kiện sort "tại chỗ".

5. Delegate và Lambda – cặp đôi hoàn hảo

Có thể bạn sẽ hỏi: vậy lambda có phải là delegate không, hay là cái gì khác?

Thực ra, lambda chỉ là syntactic sugar (đường cú pháp) để tạo instance delegate hoặc expression tree (cái này nói sau). Khi compiler thấy lambda, nó sẽ tự động chuyển thành instance delegate phù hợp. Trong ví dụ của ta, vì List<T>.Sort() cần delegate Comparison<T>, compiler hiểu rằng (student1, student2) => student1.Age.CompareTo(student2.Age) phải chuyển thành Comparison<Student>.

Vậy nên, lambda giúp ta viết code cực kỳ ngắn gọn, còn delegate là "container" mang code đó đi và thực thi. Chúng phối hợp rất ăn ý!

Khi nào dùng cái nào?

  • IComparable<T>: Dùng khi kiểu dữ liệu của bạn có thứ tự tự nhiên, rõ ràng. Ví dụ, sort sản phẩm theo mã hàng. Interface này định nghĩa thứ tự "mặc định".
  • IComparer<T>: Dùng khi bạn cần logic so sánh tái sử dụng nhiều lần, nhưng không muốn "làm bẩn" class chính hoặc có nhiều kiểu sort khác nhau. Ví dụ, một IComparer sort sản phẩm theo giá, một cái khác theo tên, dùng ở các chỗ khác nhau trong chương trình.
  • Delegate (Comparison<T>) và Lambda: Lý tưởng cho sort một lần, ad-hoc, khi logic so sánh đơn giản, không cần class riêng. Đây là cách phổ biến và sạch nhất cho đa số bài toán sort trong C#. Cũng là cách tuyệt vời để truyền logic vào method khác, ví dụ các method lọc (Find, FindAll) hoặc tìm kiếm (FindIndex) mà ta đã học trước đó.
Đặc điểm IComparable<T> IComparer<T> Comparison<T> / Lambda
Xác định ở đâu? Trong chính class T Trong class-comparator riêng Có thể là method hoặc biểu thức ẩn danh
Linh hoạt Thứ tự "tự nhiên" cố định Nhiều thứ tự, tái sử dụng được Ad-hoc (tại chỗ), cho từng lần gọi method
Bôi trơn code Ít, nằm trong class Trung bình (class riêng) Tối thiểu (đặc biệt với lambda)
Ví dụ dùng
someList.Sort()
someList.Sort(new MyComparer())
someList.Sort((x, y) => x.Prop.CompareTo(y.Prop))
Dễ đọc Tốt cho thứ tự tự nhiên Phụ thuộc tên comparator Rất tốt cho so sánh đơn giản, đặc thù

6. Ứng dụng thực tế và nhìn về tương lai

Lambda không chỉ là "đường cú pháp" cho sort. Nó là công cụ mạnh mẽ, dùng khắp nơi trong code C# hiện đại. Bạn sẽ gặp nó rất nhiều:

  • Trong LINQ (Language Integrated Query): Đây có lẽ là nơi lambda dùng nhiều nhất. LINQ cho phép viết truy vấn kiểu SQL cho collection, và lambda dùng để định nghĩa điều kiện lọc, sort, chuyển đổi dữ liệu. Sắp tới ta sẽ học LINQ, bạn sẽ thấy lambda làm nó mạnh và tiện thế nào.
  • Trong xử lý sự kiện: Lambda giúp viết gọn logic xử lý khi có sự kiện (ví dụ bấm nút trong UI).
  • Trong lập trình bất đồng bộ: Để định nghĩa task chạy song song.
  • Trong nhiều API .NET: Nhiều method trong thư viện .NET nhận delegate (và do đó, lambda) làm tham số để thêm logic linh hoạt.

Vậy nên, nắm vững lambda, bạn không chỉ sort giỏi hơn mà còn hiểu sâu code C# hiện đại và các thư viện. Kỹ năng này sẽ rất được đánh giá cao khi phỏng vấn và hữu ích trong mọi project!

7. Lỗi thường gặp và lưu ý

Khi làm việc với delegate và lambda để so sánh, có vài điểm bạn nên chú ý:

Kết quả so sánh sai: Nhớ rằng CompareTo hoặc logic so sánh của bạn phải trả về số âm, 0 hoặc số dương. Nếu trả về cái khác, sort có thể sai hoặc lỗi. Lỗi phổ biến nhất là newbie trả về true hoặc false thay vì int. Method Sort cần kết quả số, vì nó cần biết không chỉ hai phần tử bằng nhau mà còn phần tử nào "lớn hơn".

Xử lý giá trị null: Nếu phần tử trong collection có thể là null, gọi method trên object null (ví dụ student1.Age.CompareTo(...) nếu student1null) sẽ gây NullReferenceException. Khi đó, logic so sánh của bạn nên xử lý rõ ràng trường hợp null. Theo quy tắc chung, null được coi là "nhỏ hơn" mọi giá trị khác. Nếu cả hai null, chúng bằng nhau. Nếu một null, một không, null "nhỏ hơn".

// Ví dụ xử lý null trong lambda so sánh
students.Sort((s1, s2) => {
    if (s1 == null && s2 == null) return 0;
    if (s1 == null) return -1; // null nhỏ nhất
    if (s2 == null) return 1;  // không-null lớn hơn null
    return s1.Age.CompareTo(s2.Age); // So sánh nếu cả hai không null
});

May mắn là, thực tế collection thường không có null, nhưng nhớ điểm này rất quan trọng!

Hiệu năng: Dù lambda rất tiện, nếu lạm dụng trong vòng lặp "nóng" hoặc collection cực lớn thì có thể hơi ảnh hưởng hiệu năng so với class IComparer tối ưu, đã test kỹ. Tuy nhiên, với đa số bài toán thường ngày, chênh lệch không đáng kể, còn lợi ích về code gọn, dễ đọc thì vượt trội.

Chuỗi so sánh phức tạp: Như ví dụ sort theo họ rồi tên, lambda cho phép lồng nhiều điều kiện. Tiện hơn nhiều so với viết cả chục if trên một dòng! Quan trọng là luôn kiểm tra kết quả so sánh đầu tiên (lastNameComparison != 0) rồi mới đến điều kiện tiếp theo.

Lambda và delegate là khái niệm nền tảng trong C#, mở ra phong cách lập trình linh hoạt, functional hơn. Hiểu và dùng tốt chúng sẽ giúp code của bạn sạch, hiệu quả và hiện đại hơn rất nhiều. Cứ thử nghiệm đi, rồi bạn sẽ dùng chúng như bản năng!

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION