程式語言 · 泛型約束

# 約束：先講好入場資格，方法裡才能用（C# where）

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&lt;T&gt;入場資格：要會 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()。

## 常見的坑

- **`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 對照

| Go | T 必須是 |
| --- | --- |
| `[T any]` | 任何型別（幾乎什麼都不能做） |
| `[T comparable]` | 能用 `==` 比相等的型別 |
| `[T cmp.Ordered]` | 能用 `< >` 比大小的型別 |

Go 也要先講好 T 會什麼，只是寫法不同：它在方括號裡寫「T 必須屬於哪一類型別」，例如 `cmp.Ordered` 就是「會用 &lt; &gt; 比大小的那一類」。細節見 [Go 官方泛型教學](https://go.dev/doc/tutorial/generics)。

## ✔ 過關標準

能不看筆記回答下面 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）。

## 參考來源

- [Microsoft Learn — Constraints on type parameters](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters)（各種約束、沒約束只能用 object 的成員）
- [Microsoft Learn — where (generic type constraint)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/where-generic-type-constraint)（new() 不能和 struct 併用）
- [Go — Tutorial: Getting started with generics](https://go.dev/doc/tutorial/generics)、[package cmp](https://pkg.go.dev/cmp)（comparable、cmp.Ordered）
