C# 4.0 引入泛型協變與逆變,是型別系統邁向更靈活多態的重要一步。但許多開發者僅知 out/in 關鍵字,卻不清其背後的「契約」與「輸入/輸出」本質。本文將從委派、介面兩大應用場景切入,層層剖析協變/逆變的核心邏輯與設計哲學。
設 Bar : Foo,則 Foo 為弱型別(基類),Bar 為強型別(衍生類)。
out T)。in T)。delegate TResult Func(); // 唯讀位置
Func fBar = () => new Bar();
Func fFoo = fBar; // OK: Bar → Foo
Foo foo = fFoo(); // 呼叫端只「讀取」Foo,實際回傳 Bar 符合 Liskov 原則
delegate void Action(T arg); // 唯寫位置
Action aFoo = (foo) => Console.WriteLine(foo);
Action aBar = aFoo; // OK: Foo → Bar
aBar(new Bar()); // 呼叫端只「傳入」Bar,實際處理 Foo 亦安全
interface IReadOnlyList : IEnumerable { T this[int i] { get; } }
IReadOnlyList bars = new List();
IReadOnlyList foos = bars; // OK,僅提供「取出」操作
interface IComparer { int Compare(T x, T y); }
IComparer cmpFoo = new FooComparer();
IComparer cmpBar = cmpFoo; // OK,僅提供「輸入」操作
Func<in T, out TResult> 完美詮釋:
因此 輸入參數不可標註 out,回傳值不可標註 in,否則違反型別安全。out/ref 參數兼具輸入輸出,故不可標註任何變異註記。
逆變讓我們為基類定義一次演算法(如排序、比較、繪圖),自動適用於所有衍生類別,體現「多態」精髓:
interface IPaintable { Color Color { get; set; } }
interface IBrush where T : IPaintable { void Paint(T obj, Color c); }
class Brush : IBrush where T : IPaintable { ... }
// 一把「能塗 IPaintable」的刷子,自動能塗 Car、House 等所有衍生類
IBrush brush = new Brush();
IBrush carBrush = brush; // 逆變指派
carBrush.Paint(new Car(), Color.Red);
協變 = 「繼承」的延伸(子類實例可當父類用);逆變 = 「多態」的深化(父類邏輯可處理子類輸入)。掌握 in = 輸入 = 逆變、out = 輸出 = 協變,即可在設計泛型介面/委派時遊刃有餘。