假設你想寫一個 Max,丟兩個東西進去,回傳比較大的那個。數字要能用,字串、日期也要能用。學過泛型之後,你很自然會這樣寫:
static T Max<T>(T a, T b) => a.CompareTo(b) > 0 ? a : b; // 編譯錯誤
<T> 代表「呼叫的時候才決定的型別」。看起來沒問題,編譯器卻不讓你過。原因是 T 可以是任何型別,有些型別根本不會比大小,例如一個普通的 object。編譯器沒辦法保證每個 T 都有 CompareTo,所以乾脆不讓你呼叫。
一句話:約束(constraint)就像替 T 訂一個入場資格。你先講好「能當 T 的,一定會比大小」,編譯器才放心讓你在方法裡用 CompareTo。
C# 的每個型別都繼承自 object。所以沒有約束時,編譯器只讓你用 object 身上的方法,例如 Equals、ToString。其他的,就算你傳進來的 int 明明會,編譯器也不認。
圖 A:沒有約束的 T,只能用 object 有的東西(點圖可放大)
這段在講:編譯器檢查的是「每一個可能的 T 都會什麼」,而不是你這次剛好傳了什麼。你這次傳 int 沒用,它要顧到所有的情況。
在方法後面加 where,講好 T 必須實作 IComparable<T>(也就是「會 CompareTo」):
static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) > 0 ? a : b; // 現在可以了
圖 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()。
struct 已經保證能 new,所以不能再加 new()。寫 where T : struct, 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 | T 必須是 |
|---|---|
[T any] | 任何型別(幾乎什麼都不能做) |
[T comparable] | 能用 == 比相等的型別 |
[T cmp.Ordered] | 能用 < > 比大小的型別 |
Go 也要先講好 T 會什麼,只是寫法不同:它在方括號裡寫「T 必須屬於哪一類型別」,例如 cmp.Ordered 就是「會用 < > 比大小的那一類」。細節見 Go 官方泛型教學。
能不看筆記回答下面 2 題,這篇就過了。
where T : IComparable<T>,編譯器就確定每個 T 都會 CompareTo。