CodeGym /課程 /C# SELF /用於比較的 Lambda 表達式與委派

用於比較的 Lambda 表達式與委派

C# SELF
等級 30 , 課堂 2
開放

1. 前言

想像一下你有一個學生清單,你要先按年齡排序,再按姓氏,然後再按平均分數,但只針對那些通過所有考試的人。如果每次這種「一次性」的比較都要寫一個新的 IComparer<T> 類別,你的專案很快就會變成一堆小比較器類別的大雜燴。這樣很麻煩,程式碼又冗長又難讀。

這種情況下,C# 給了我們一個更優雅的解法:可以直接把比較邏輯傳給 Sort 方法,不用另外寫類別。

這時我們就會用到 委派 和它的精簡兄弟——lambda 表達式

委派——我們的彈性小幫手

在我們進入 lambda 之前,先搞懂什麼是 委派。簡單說,委派就是一種可以存放方法參考的型別。聽起來有點玄?這樣想吧:

想像你有一張待辦清單,有些項目是「指令」或「食譜」。委派就像一個特別的變數,可以存一個「食譜」(方法)的參考。然後你要做這件事時,只要呼叫這個委派變數,它就會「叫」那個方法。

在 C# 裡,委派常用來做 callback(之後再呼叫方法,通常是事件反應)、事件處理(像是按鈕點擊),當然也可以把方法當參數傳給其他方法(讓方法可以呼叫另一個方法),這正是我們現在排序要用的。

List<T>.Sort() 這個方法有好幾個 overload(版本),其中一個就是吃一個叫 Comparison<T> 的委派。

2. Comparison<T> 委派

什麼是 Comparison<T>

Comparison<T> 是 .NET 內建的委派,專門拿來比較兩個同型別 T 的物件。它的「食譜」長這樣:吃進兩個 T 型別的物件(我們叫他們 xy),回傳一個整數(int):

  • 負數(例如 -1),如果 x「小於」y
  • 0),如果 x「等於」y
  • 正數(例如 1),如果 x「大於」y

這套規則跟 IComparable.CompareToIComparer.Compare 一樣。也就是說,邏輯一樣,只是現在我們可以把它當「方法變數」傳,不用寫一個類別。

來看個例子,回到我們的學生。如果有個 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($"學生: {FirstName} {LastName}, 年齡: {Age}, 分數: {AverageGrade:F2}");
    }
}

現在要用委派按年齡排序學生清單,我們可以寫一個靜態方法,符合 Comparison<Student> 的簽名:

public class Program
{
    // 這個方法符合 Comparison<Student> 委派的簽名
    // 它會比較兩個學生的年齡
    public static int CompareStudentsByAge(Student student1, Student student2)
    {
        // 用內建的 CompareTo 方法,
        // 會根據比較結果回傳 -1, 0 或 1。
        return student1.Age.CompareTo(student2.Age);
    }

    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("伊凡", "彼得羅夫", 20, 4.5),
            new Student("瑪麗亞", "西多羅娃", 22, 4.8),
            new Student("阿列克謝", "伊萬諾夫", 19, 3.9),
            new Student("葉蓮娜", "科茲洛娃", 20, 4.2) // 兩個學生同年齡
        };

        Console.WriteLine("--- 排序前的學生清單 ---");        
        foreach (var s in students)
            s.PrintInfo();

        Console.WriteLine("--- 用委派按年齡排序學生 ---");
        students.Sort(CompareStudentsByAge); //把 CompareStudentsByAge 方法當參數傳進去

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

來拆解一下:

  1. 我們寫了一個靜態方法 CompareStudentsByAge,吃兩個學生,回傳 int,符合 Comparison<Student> 的規則。
  2. Main 裡建立學生清單。
  3. 呼叫 students.Sort(CompareStudentsByAge); 時,我們不是馬上執行 CompareStudentsByAge(),而是傳遞方法參考List<T>.Sort() 會自己多次呼叫我們的方法,傳不同的學生組合進去。這就像你給人家一個送貨地址,而不是直接把整台卡車開過去。

這種寫法比每次都寫一個比較器類別方便多了。但還可以更精簡!

3. 來認識 Lambda 表達式

就算寫一個像 CompareStudentsByAge 這樣的獨立方法,有時也嫌多餘,尤其比較邏輯很簡單、只用一兩次。這時 C# 就有 lambda 表達式(lambda expressions)。

什麼是 lambda 表達式?其實就是匿名方法,或者我喜歡開玩笑說「無家可歸的方法」。你可以直接在需要的地方寫一小段方法,不用另外宣告。就像直接寫個便條貼貼在任務上,而不是寫一本說明書。

lambda 表達式的主要符號是 =>(唸作「箭頭」或「轉到」)。它把參數和方法主體分開。

lambda 表達式的基本語法

假設你有個委派(方法參考)要傳給 Sort()

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

students.Sort(CompareStudentsByAge); //把 CompareStudentsByAge 方法當參數傳進去

其實可以更短:

//直接把匿名方法當參數傳進去
students.Sort( (Student student1, Student student2) => student1.Age.CompareTo(student2.Age) );

這裡我們沒寫方法名,只寫了兩個重點:

  • 參數:(Student student1, Student student2)
  • 方法內容:student1.Age.CompareTo(student2.Age)

這種精簡寫法就叫 lambda 表達式(參數) => 表達式

它怎麼運作

C# 編譯器遇到 lambda 表達式時,會自動幫你生出一個真正的方法。

假設你有這樣的程式碼:

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

編譯結果大概會像這樣:

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

students.Sort( CompareStudents_Lambda123 );

4. 排序範例與 lambda 表達式

我們把學生的例子改寫成 lambda 表達式版本:

public class Program
{
    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("伊凡", "彼得羅夫", 20, 4.5),
            new Student("瑪麗亞", "西多羅娃", 22, 4.8),
            new Student("阿列克謝", "伊萬諾夫", 19, 3.9),
            new Student("葉蓮娜", "科茲洛娃", 20, 4.2)
        };

        Console.WriteLine("--- 排序前的學生清單 ---");        
        foreach (var s in students)
            s.PrintInfo();

        // 現在比較邏輯直接寫在這裡,「就地」完成
        Console.WriteLine("--- 用 lambda 表達式按年齡排序學生 ---");
        students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));

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

        // 要按分數遞減排序,只要把結果乘 -1
        Console.WriteLine("\n--- 用平均分數遞減排序學生 ---");
        // s2.CompareTo(s1) 取代 s1.CompareTo(s2)
        students.Sort((s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade));

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

這裡發生了什麼?

  1. students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));
    • student1student2Sort 會傳給我們匿名方法的參數(就像 xyComparison<T>)。
    • => 是 lambda 運算子。
    • student1.Age.CompareTo(student2.Age) 是 lambda 的主體。這裡只有一個表達式,結果就是回傳值。
  2. 要按平均分數遞減排序,只要把 s1s2CompareTo 裡對調。這是反轉排序順序的經典招。

為什麼這麼方便?

  • 精簡: 不用為每個小比較邏輯寫方法或類別。
  • 可讀性: 比較邏輯就在 Sort() 旁邊,簡單明瞭,特別適合簡單情境。
  • 彈性: 可以隨時「現場」改排序條件。

5. 委派與 Lambda——絕配

你可能會問:lambda 表達式跟委派是一樣的東西嗎?

其實 lambda 表達式只是語法糖(syntax sugar),用來建立委派實例或表達式樹(Expression Tree,這以後再說)。編譯器看到 lambda,就會自動把它變成對應的委派實例。我們這裡 List<T>.Sort() 需要 Comparison<T>,所以 (student1, student2) => student1.Age.CompareTo(student2.Age) 會被轉成 Comparison<Student>

所以 lambda 讓我們寫出超精簡的程式碼,而委派就是那個「容器」把這段程式碼帶著走、執行。他們是最佳拍檔!

什麼時候用哪個?

  • IComparable<T>:當你的型別有自然、明顯的排序方式時用。例如商品主要用貨號排序。這個介面定義「預設」順序。
  • IComparer<T>:當你需要多次、可重用的比較邏輯,但又不想「污染」主類別,或有多種排序方式時用。例如一個 IComparer 按價格,一個按名稱,在不同地方用。
  • 委派(Comparison<T>)和 Lambda: 最適合一次性、臨時的排序,邏輯簡單不需要獨立類別。這是 C# 排序最常見、最乾淨的寫法。也很適合傳遞邏輯給其他方法,例如過濾(Find, FindAll)或搜尋(FindIndex)等方法(我們之前有學過)。
特點 IComparable<T> IComparer<T> Comparison<T> / Lambda 表達式
定義在哪? 在類別 T 在獨立比較器類別 可以是方法或匿名表達式
彈性 固定「自然」順序 多種、可重用順序 臨時(現場)、針對單次方法呼叫
樣板程式 少,寫在類別裡 中等(獨立類別) 最少(lambda 最精簡)
使用範例
someList.Sort()
someList.Sort(new MyComparer())
someList.Sort((x, y) => x.Prop.CompareTo(y.Prop))
可讀性 適合自然順序 看比較器名稱而定 簡單、明確,適合特定比較

6. 實務應用與未來展望

lambda 表達式不只是排序的「語法糖」。它是現代 C# 程式碼裡超強大的工具,你會常常遇到:

  • 在 LINQ(Language Integrated Query): 這大概是 lambda 最常見的應用。LINQ 讓你寫 SQL 風格的查詢,lambda 用來定義過濾、排序、資料投影等條件。我們很快就會學 LINQ,到時你會看到 lambda 有多強大又方便。
  • 事件處理: lambda 可以很簡潔地描述事件發生時要做什麼(例如 UI 按鈕點擊)。
  • 非同步程式: 用來定義要平行執行的任務。
  • 各種 .NET API: 很多 .NET 標準函式庫的方法都吃委派(也就是 lambda),讓你可以加上彈性的邏輯。

所以學會 lambda,不只讓你排序更強,還能幫你理解現代 C# 程式和函式庫。這是每個面試都會加分、每個專案都用得到的技能!

7. 常見錯誤與細節

用委派和 lambda 做比較時,有幾個重點要注意:

比較結果錯誤: 記得 CompareTo 或你的比較邏輯一定要回傳負數、零或正數。如果你不小心回傳了別的,排序可能會亂掉甚至出錯。最常見的新手錯誤是回傳 truefalse,而不是 intSort 需要數字結果,因為它不只要知道元素是否相等,還要知道誰「大」。

處理 null 值: 如果你的集合裡可能有 null,那對 null 物件呼叫方法(像 student1.Age.CompareTo(...),如果 student1null)會噴 NullReferenceException。這時你的比較邏輯要明確處理 null。一般規則是 null 算「小於」任何非 null。如果兩個都是 null,就相等。如果一個 null,另一個不是,null「小」。

// lambda 比較時處理 null 的範例
students.Sort((s1, s2) => {
    if (s1 == null && s2 == null) return 0;
    if (s1 == null) return -1; // null 最小
    if (s2 == null) return 1;  // 非 null 比 null 大
    return s1.Age.CompareTo(s2.Age); // 兩個都不是 null 就比較
});

幸好,實務上集合很少有 null,但這點還是很重要!

效能: 雖然 lambda 超方便,但如果你在很「熱」的迴圈或超大集合裡狂用,效能可能會比經過優化的 IComparer 類別差一點點。不過大多數情況下差異很小,精簡和可讀性才是王道。

複雜的多層比較: 像我們之前按姓氏再按名字排序的例子,lambda 可以很方便地寫多層條件。這比寫一堆 if 還要好!重點是先判斷第一層比較結果(lastNameComparison != 0),再進下一層。

lambda 和委派是 C# 的基礎觀念,讓你寫出更彈性、更函數式的程式。學會用它們,你的程式會更乾淨、有效率又現代。多多練習,很快你就會用得很順手啦!

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION