程式語言 · Covariance 與 Contravariance

# 貓的名單能不能當動物名單用？

Chance Lu

假設你寫了一個印清單的方法，參數是 `IEnumerable<object>`，什麼都能印。你手上有一串貓的名字，型別是 `List<string>`，傳進去完全沒問題。

後來你把參數改成 `List<object>`，同一串名字就傳不進去了，編譯器直接畫紅線。**string 明明是一種 object**，為什麼一個可以、一個不行？

差別就在：這個型別**讓不讓你往裡面放東西**。

> 可以把泛型想成**籠子**。**只能往外拿**的籠子，貓籠拿來當動物籠沒問題；**能往裡放**的就不行，因為別人看到「動物籠」的標籤，可能會塞一隻狗進去。

下面都用同一套比喻：**string 是貓、object 是動物、1（int）是狗**。1 也是 object，但不是 string，就像狗是動物，但不是貓。

| 名詞 | 白話 |
| --- | --- |
| Invariance（不變） | 預設規則：`<>` 裡的型別必須一模一樣，`List<string>` 就只能當 `List<string>` |
| Covariance（共變，`out`） | 只往外給東西的型別：`<string>` 可以當 `<object>` 用 |
| Contravariance（逆變，`in`） | 只往裡收東西的型別：方向反過來，`<object>` 可以當 `<string>` 用 |
| 委派（delegate） | 把「一個方法」存成變數的型別，例如 `Func`、`Action` |

## 第一步：List&lt;string&gt; 為什麼不能當 List&lt;object&gt;

你早就知道 `object o = "咪咪";` 沒問題，所以很自然會以為整個清單也能這樣換。

問題是**清單不只能讀，還能 Add**。假設編譯器放行，看看會發生什麼事：

1. `cats` 是貓的清單，裡面有「咪咪、小花」。
2. `List<object> cage = cats;`：同一個籠子貼了兩張標籤，`cats` 寫「貓」，`cage` 寫「動物」。
3. 從 `cage` 看，這是動物籠，所以 `cage.Add(1)` 完全合法，狗就這樣被塞進去了。
4. 拿 `cats` 的人以為裡面全是貓，`cats[2]` 卻拿到 1。

① 貓的清單
cats：List&lt;string&gt;
"咪咪"
"小花"
② 假設放行
cage = cats
cats＝貓
cage＝動物
"咪咪"
"小花"
同一個籠子，兩張標籤
③ 照動物籠塞
cage.Add(1)
"咪咪"
"小花"
1（狗）
④ 拿貓拿到狗
string s = cats[2]
"咪咪"
"小花"
1
✕ 1 不是 string

實際上 C# 在第 ② 步就擋下（error CS0029），③ ④ 根本不會發生

圖 A：如果放行，狗會從「動物」那張標籤被塞進貓籠（動畫依序點亮四步，點圖可放大）

所以 C# 在第 2 步就不讓你過。`List<T>` 的 T 是 **invariant**（不能換）：`<>` 裡寫什麼，就只能當什麼用。

> 能往裡放的籠子換上更大的標籤，等於讓別人有機會塞錯東西進來。

## 第二步：只往外拿，就可以換大標籤（out）

可是很多時候你只是要讀，像開頭那個印清單的方法，根本不會往裡面塞東西。連這種情況都擋掉，就太嚴格了。

這時就輪到 `out`。在 T 前面加 out，等於跟編譯器**保證**：T 只會出現在「往外給」的位置，例如回傳值。

- **`IEnumerable<out T>`**：只能一個一個往外拿，沒有 Add。從貓籠裡拿出來的一定是貓，貓當然是動物，所以 `IEnumerable<object> look = cats;` 可以。
- **`Func<out TResult>`**：回傳值也是往外給。一個會交出貓的方法，說它「會交出動物」也沒錯。

這個保證不是說說而已，編譯器會檢查。如果你在標了 `out T` 的介面裡寫 `void Put(T item)`，會直接編譯錯誤（練習 3 會看到）。

> **out 就是只出不進**，所以 &lt;貓&gt; 可以當 &lt;動物&gt; 用。這就叫 covariance。

## 第三步：只往裡收，方向反過來（in）

有些型別剛好相反，只收不吐。例如 `Action<T>`：你給它一個值，它去做事，什麼都不回傳。

這種型別用的是 `in`：**T 只出現在「往裡收」的位置**，例如參數。能換的方向也剛好和 out 相反。

- **`Action<in T>`**：`Action<object> print` 什麼都能印，就像什麼動物都收的收容所。把它放到需要 `Action<string>` 的地方很安全，因為送進來的貓本來就是動物。反過來就不行：只會照顧貓的地方被當成什麼都收，就可能有人送狗進來，它應付不了。
- **`IComparer<in T>`**：比較器只接收兩個值、回傳一個 int。能比較任何 object 的比較器，拿來排 string 當然也行。

Covariance：只有出口
IEnumerable&lt;out T&gt;、Func&lt;out T&gt;

貓籠 &lt;string&gt;只能往外拿

拿出來的一定是動物
✔ &lt;string&gt; 能當 &lt;object&gt;
Contravariance：只有入口
Action&lt;in T&gt;、IComparer&lt;in T&gt;

收容口 &lt;object&gt;什麼動物都收

送進來的貓也是動物
✔ &lt;object&gt; 能當 &lt;string&gt;
Invariance：兩邊都開
List&lt;T&gt;

貓籠 &lt;string&gt;能拿也能放

有人會照動物籠塞狗
✕ 型別要一模一樣

圖 B：看籠子開了哪個門，決定標籤能往哪邊換（點圖可放大）

> 整理一下：**out 是 &lt;貓&gt; 可以當 &lt;動物&gt;；in 是 &lt;動物&gt; 可以當 &lt;貓&gt;；兩個方向都用到的，就必須一模一樣。**Covariance 和 contravariance 講的就是這些。

## 第四步：陣列是例外，錯到執行時才爆

照第一步的道理，陣列能讀也能寫，`string[]` 不該能當 `object[]`。但 C# 從 1.0 起就允許，這叫 array covariance。

那放錯東西時誰來擋？陣列和泛型在這裡走了不同的路。

陣列的做法是在籠子門口**站一個警衛**：每次寫入，執行時都會檢查這個值符不符合陣列原本的型別，不符合就丟出 `ArrayTypeMismatchException`。

object[] arr = new string[2];　編譯器放行
arr[0] = "咪咪"
arr[1] = 1

門口警衛（執行時）這個值真的是 string 嗎？

標籤：object[]
真身：string[]"咪咪" 放進去了

✕ ArrayTypeMismatchException
泛型 List 在寫程式時就擋（圖 A）；陣列放行，每次寫入才檢查

圖 C：陣列把檢查留到執行時，放錯要跑到那一行才爆（點圖可放大）

為什麼陣列要這樣？C# 編譯器團隊的 Eric Lippert 說明過：.NET 執行環境為了支援 Java 這類語言加了 array covariance，C# 也就跟著沿用。這樣做有兩個代價：

- 本來編譯器抓得到的錯，變成**執行時才爆**。
- 每次寫入都要多做一次**檢查**。

泛型介面和委派的 covariance、contravariance 是後來（C# 4）才加的，做法不一樣：**編譯時就確保安全**，不用靠警衛。

> 陣列是先放行，執行時再檢查；泛型只放行安全的方向，編譯時就檢查完。

## 兩個限制：只有介面和委派、只有參考型別

看到這裡，你可能會想把自己寫的 class 也標上 out，或讓 int 清單也支援 covariance。這兩件事都做不到：

- **只有介面和委派能標 in / out。**class 不行，所以 `List<T>` 永遠是 invariant。要用 covariance，就改用它實作的介面，例如 `IEnumerable<T>`、`IReadOnlyList<T>`。
- **只有參考型別**（class、string 這類，變數裡放的是地址，[前篇](https://docs.smartech.tw/n/generics-boxing-list-arraylist)講過）。`IEnumerable<int>` 不能當 `IEnumerable<object>`。

為什麼 int 不行？string 當 object 用，東西還是**同一個**，只是換張標籤。int 要變成 object 卻得[裝箱](https://docs.smartech.tw/n/generics-boxing-list-arraylist)，每個元素都要另外包一個盒子。Covariance 只會換標籤，不會幫你一個一個裝箱。

> Covariance 和 contravariance 只是換標籤，所以只適用於介面、委派，以及 class、string 這類參考型別。

## 常見的坑

- **方法參數寫成 `List<object>`，別人就傳不進 `List<string>`。**如果方法只會讀，參數就改成 `IEnumerable<object>` 或 `IReadOnlyList<object>`。
- **看到錯誤訊息寫「An explicit conversion exists」，別真的去加轉型。**`(IEnumerable<object>)nums` 編譯會過，執行時卻會丟 `InvalidCastException`（實際跑過）。真的要轉，就自己一個一個轉：`nums.Select(n => (object)n)`。

## 動手做

先開一個練習資料夾：

```
mkdir lab && cd lab && dotnet new console
```

每個練習都把 `Program.cs` 整個換掉。**先猜，再跑，再對答案。**下面的輸出都是在 .NET 10 實際跑出來的。

### 練習 1：陣列放錯，什麼時候爆？

建檔

```
object[] arr = new string[2];   // 編譯器放行
arr[0] = "咪咪";                 // OK：本來就是 string
Console.WriteLine("第一格放好了");
arr[1] = 1;                      // 1 是 object，但不是 string
Console.WriteLine("第二格也放好了");
```

先猜編譯得過嗎？會印出幾行？

執行`dotnet run`

```
第一格放好了
Unhandled exception. System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array.
```

這證明編譯時完全沒擋，跑到第 4 行才被門口的警衛攔下（圖 C）。

### 練習 2：清單換標籤，編譯器擋哪幾行？

建檔

```
var cats = new List<string> { "咪咪", "小花" };

IEnumerable<object> look = cats;   // 只能往外拿
foreach (var x in look) Console.WriteLine(x);

List<object> cage = cats;          // 能往裡放
cage.Add(1);

var nums = new List<int> { 1, 2 };
IEnumerable<object> boxed = nums;  // int 不是參考型別
```

先猜第 3、6、10 行，哪幾行會錯？

執行`dotnet build`

```
Program.cs(6,21): error CS0029: Cannot implicitly convert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.List<object>'
Program.cs(10,29): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.List<int>' to 'System.Collections.Generic.IEnumerable<object>'. An explicit conversion exists (are you missing a cast?)
```

你會看到第 3 行沒錯（out，只出不進）；第 6 行錯（圖 A 的第 ② 步）；第 10 行錯（int 要裝箱，不能只換標籤）。

### 練習 3：out 和 in 都能編譯

建檔

```
// out：只往外給，Func<string> 可以當 Func<object>
Func<string> getCat = () => "咪咪";
Func<object> getAny = getCat;
Console.WriteLine($"out：拿到 {getAny()}");

// in：只往裡收，Action<object> 可以當 Action<string>
Action<object> print = o => Console.WriteLine($"in：收到 {o}");
Action<string> printCat = print;
printCat("小花");

// in：會比任何 object 的比較器，也能拿來排 string
IComparer<object> byText = Comparer<object>.Create(
    (a, b) => string.CompareOrdinal(a.ToString(), b.ToString()));
IComparer<string> forCats = byText;
var names = new List<string> { "Tom", "Amy", "Leo" };
names.Sort(forCats);
Console.WriteLine("排好：" + string.Join(", ", names));
```

執行`dotnet run`

```
out：拿到 咪咪
in：收到 小花
排好：Amy, Leo, Tom
```

再試自己寫一個 out 介面，卻放了「往裡放」的方法：

```
Console.WriteLine("hi");

interface ICage<out T>
{
    T Take();            // 往外給：OK
    void Put(T item);    // 往裡放：out 不准
}
```

`dotnet build`：

```
Program.cs(6,14): error CS1961: Invalid variance: The type parameter 'T' must be contravariantly valid on 'ICage<T>.Put(T)'. 'T' is covariant.
```

這證明out 不是隨便標的：標了只出不進，編譯器就不讓你偷開入口。

### Go 對照

Go 的泛型**沒有** covariance 和 contravariance。`[]string` 不能當 `[]any`，要轉就得自己一個一個複製過去（見 [Go FAQ](https://go.dev/doc/faq)）。

## ✔ 過關標準

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

1. IEnumerable&lt;string&gt; 可以當 IEnumerable&lt;object&gt;，List&lt;string&gt; 卻不能當 List&lt;object&gt;，差在哪？
**差在能不能往裡放。**
    - IEnumerable 只出不進（`out T`），拿出來的 string 一定是 object。
    - List 能 Add。如果放行，別人會照 `List<object>` 的標籤塞一個 1 進去，拿 string 的人就拿到 1（圖 A）。
2. 為什麼 Action&lt;object&gt; 能當 Action&lt;string&gt;，反過來卻不行？
**Action 只往裡收（`in T`），方向和 out 相反。**
    - 會處理任何 object 的方法，送 string 進去一定處理得了。
    - 只會處理 string 的方法，被當成 `Action<object>` 後可能收到 1，它處理不了。
3. `object[] arr = new string[2]; arr[0] = 1;` 為什麼編譯得過、執行才爆？泛型為什麼不這樣設計？
**陣列從 C# 1.0 就允許 covariance，放錯靠執行時的檢查擋（ArrayTypeMismatchException）。**
    - 這是 .NET 執行環境為支援 Java 類語言加的，代價是錯誤晚發現、每次寫入都要檢查。
    - 泛型的 covariance、contravariance 只放行安全的方向，在編譯時就擋掉。
4. 為什麼 IEnumerable&lt;int&gt; 不能當 IEnumerable&lt;object&gt;？自己寫的 class 能標 out 嗎？
**Covariance 只換標籤，int 變 object 要裝箱，不是換標籤；class 不能標。**
    - in / out 只能用在介面和委派，而且只對參考型別有效。
    - int 要轉就自己做：`nums.Select(n => (object)n)`。

## 參考來源

- [Microsoft Learn — Covariance and Contravariance in Generics](https://learn.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance)（invariance／covariance／contravariance 定義、只限介面與委派、只限參考型別、內建 variant 型別清單）
- [Microsoft Learn — Covariance and Contravariance (C#)](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/)（IEnumerable、Action 範例；array covariance 不是型別安全）
- [C# 語言規格 §19.2.3 Variant type parameter lists](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/interfaces#19231-general)（in / out 只能用在介面與委派、variance safety、需要參考轉換）
- [C# 語言規格 §17.6 Array covariance](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/arrays#176-array-covariance)（寫入時的執行期檢查、ArrayTypeMismatchException、不適用實值型別）
- [Eric Lippert — Array Covariance（Microsoft Learn 封存部落格）](https://learn.microsoft.com/en-us/archive/blogs/ericlippert/covariance-and-contravariance-in-c-part-two-array-covariance)（array covariance 為何存在：CLR 為支援 Java 類語言）
- [Go FAQ](https://go.dev/doc/faq)（Go 泛型沒有 covariance／contravariance；`[]T` 不能直接轉 `[]interface{}`）
