程式語言 · 泛型約束

約束:先講好入場資格,方法裡才能用(C# where)

Chance Lu · 學習文章 · 2026-11-02 22:04
Chance Lu

假設你想寫一個 Max,丟兩個東西進去,回傳比較大的那個。數字要能用,字串、日期也要能用。學過泛型之後,你很自然會這樣寫:

static T Max<T>(T a, T b) => a.CompareTo(b) > 0 ? a : b;   // 編譯錯誤

<T> 代表「呼叫的時候才決定的型別」。看起來沒問題,編譯器卻不讓你過。原因是 T 可以是任何型別,有些型別根本不會比大小,例如一個普通的 object。編譯器沒辦法保證每個 T 都有 CompareTo,所以乾脆不讓你呼叫。

一句話:約束(constraint)就像替 T 訂一個入場資格。你先講好「能當 T 的,一定會比大小」,編譯器才放心讓你在方法裡用 CompareTo。

第一步:沒有約束,T 只能用每個型別都有的東西

C# 的每個型別都繼承自 object。所以沒有約束時,編譯器只讓你用 object 身上的方法,例如 Equals、ToString。其他的,就算你傳進來的 int 明明會,編譯器也不認。

沒有約束的 T可能是任何型別 能用:每個型別都有 x.Equals(y) x.ToString() ✕ 不能用:不是每個都有 a.CompareTo(b) new T() return null

圖 A:沒有約束的 T,只能用 object 有的東西(點圖可放大)

這段在講:編譯器檢查的是「每一個可能的 T 都會什麼」,而不是你這次剛好傳了什麼。你這次傳 int 沒用,它要顧到所有的情況。

第二步:用 where 貼上入場資格

在方法後面加 where,講好 T 必須實作 IComparable<T>(也就是「會 CompareTo」):

static T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) > 0 ? a : b;   // 現在可以了
誰想當 T? int string DateTime object ✕ 沒有 CompareTo → 編譯錯誤 where T : IComparable<T>入場資格:要會 CompareTo(會比大小) 方法裡面可以寫a.CompareTo(b)編譯器確定每個 T 都會

圖 B:符合資格的型別才進得來;進來的 T,方法裡就能用那個能力(點圖可放大)

這個約束兩邊都受惠。寫方法的人,在方法裡終於可以呼叫 CompareTo;呼叫的人如果想塞一個不會比大小的 object 進來,編譯時就會被擋下來(練習裡會看到這個錯誤,編號 CS0311)。

除了「要會某個方法」,還有幾種常用的約束。每加一種條件,方法裡就多一件可以做的事:

約束T 必須是方法裡多了什麼能力
where T : IComparable<T>有實作這個介面的型別可以呼叫介面上的方法,例如 a.CompareTo(b)
where T : class參考型別(class、string…)可以 return null,代表「沒找到」
where T : struct實值型別(int、DateTime、struct…)T? 變成 int? 這種可為 null 的版本,也能用 null 代表「沒有」
where T : new()有 public 無參數建構子可以 new T() 自己做一個出來
一句話:方法裡要用到 T 的什麼能力,就在 where 寫上對應的條件。要比大小就要求介面,要回傳 null 就要求 class,要 new 一個出來就要求 new()。

常見的坑

動手做

先開一個練習資料夾:

mkdir lab && cd lab && dotnet new console

把 Program.cs 整個換掉再執行。先猜、再跑、再對答案。輸出是在 .NET 10 實際跑出來的。

練習:沒有約束,三件事都做不到

建檔把 Program.cs 換成:

Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));

static T Max<T>(T a, T b)
{
    return a.CompareTo(b) > 0 ? a : b;   // T 有 CompareTo 嗎?
}

static T Make<T>()
{
    return new T();                      // T 一定能 new 嗎?
}

static T FindOrNull<T>(List<T> items, T target)
{
    foreach (var x in items)
        if (x.Equals(target)) return x;  // Equals 每個型別都有
    return null;                         // T 一定能是 null 嗎?
}

先猜哪幾行會編譯錯誤?x.Equals 會不會錯?

執行dotnet build。第一個訊息很繞,先別讀它,重點是 T 沒有保證會 CompareTo:

Program.cs(6,14): error CS7036: ... 'MemoryExtensions.CompareTo(...)'
Program.cs(11,12): error CS0304: Cannot create an instance of the variable type 'T' because it does not have the new() constraint
Program.cs(18,12): error CS0403: Cannot convert null to type parameter 'T' because it could be a non-nullable value type.

你會看到CompareTo、new T()、return null 三行都錯;x.Equals 沒錯,因為每個型別都有 Equals(圖 A)。

修好每個方法加上剛好需要的約束,再加一個 struct 版:

Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));
Console.WriteLine(Make<Box>());

var names = new List<string> { "Amy", "Bob" };
string? name = FindOrNull(names, "Cat");
if (name == null) Console.WriteLine("名字:沒找到");

var scores = new List<int> { 50, 80 };
int? score = FirstOrNull(scores, 80);
if (score == null) Console.WriteLine("分數:沒找到");
else Console.WriteLine($"分數:找到 {score}");

static T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

static T Make<T>() where T : new()
{
    return new T();
}

static T? FindOrNull<T>(List<T> items, T target) where T : class
{
    foreach (var x in items)
        if (x.Equals(target)) return x;
    return null;
}

static T? FirstOrNull<T>(List<T> items, T target) where T : struct
{
    foreach (var x in items)
        if (x.Equals(target)) return x;
    return null;   // 這裡的 T? 就是 int? 這種可為 null 的版本
}

class Box { }

dotnet run:

7
banana
Box
名字:沒找到
分數:找到 80

再試把第二行改成 Console.WriteLine(Max(new object(), new object()));,再 dotnet build:

Program.cs(2,19): error CS0311: The type 'object' cannot be used as type parameter 'T' in the generic type or method 'Max<T>(T, T)'. There is no implicit reference conversion from 'object' to 'System.IComparable<object>'.

這證明約束兩邊都管:方法裡可以放心用 CompareTo,呼叫的人也沒辦法塞一個不會比大小的 object 進來。

Go 對照

GoT 必須是
[T any]任何型別(幾乎什麼都不能做)
[T comparable]能用 == 比相等的型別
[T cmp.Ordered]能用 < > 比大小的型別

Go 也要先講好 T 會什麼,只是寫法不同:它在方括號裡寫「T 必須屬於哪一類型別」,例如 cmp.Ordered 就是「會用 < > 比大小的那一類」。細節見 Go 官方泛型教學。

✔ 過關標準

能不看筆記回答下面 2 題,這篇就過了。

  1. 為什麼沒有約束的 T 不能呼叫 CompareTo?要怎麼改?看答案收合
    T 可能是任何型別,編譯器只讓你用 object 有的方法。
    • 加上 where T : IComparable<T>,編譯器就確定每個 T 都會 CompareTo。
    • 反過來,不符資格的 object 在編譯時就被擋(CS0311)。
  2. where T : class、struct、new() 各讓方法裡多了什麼能力?看答案收合
    class:可以 return null;struct:T? 變成 int? 這種可為 null 的版本;new():可以 new T()。
    • 沒約束時,return null 和 new T() 都會編譯錯誤(實測 CS0403、CS0304)。

參考來源