协变(out)和逆变(in)是C#编译期强制校验的类型安全机制:out用于只输出(返回值)的泛型参数,支持子类→父类转换;in用于只输入(参数)的泛型参数,支持父类→子类转换;二者不可共存,且仅适用于接口和委托。
协变和逆变是 C# 中让泛型接口和委托支持“安全类型转换”的机制,不是语法糖,也不是运行时魔法——它们由编译器在编译期强制校验,核心目标只有一个:在保持类型安全的前提下,让继承关系能自然地“传导”到泛型参数上。
IEnumerable 能直接赋值给 IEnumerable
?因为 IEnumerable 声明了 T 是协变的(用 out 修饰),意味着:只要 string 是 object 的子类,那 IEnumerable 就可隐式转为 IEnumerable。这符合直觉——你从集合里“读出来”的东西,子类能当父类用(里氏替换原则)。
IEnumerablestrings = new List (); IEnumerable
IList(因为strings = new List (); IList
IList 既有 Get 又有 Set,无法同时满足协变/逆变约束)string[] → object[]),但它是**不安全的**——运行时可能抛 ArrayTypeMismatchException,而泛型协变是编译期就拦住的,更可靠。Action 能接收 Action?因为 Action 声明了 T 是逆变的(用 in 修饰),意思是:你传进去的委托,参数类型越“宽”(越靠继承链顶端),它越能接受“窄”的实际参数。比如一个能处理任意 object 的方法,当然也能安全处理 string。
Action
Func(等等——这里错了?不,funcStr = () => "a"; Func
Func 是协变的,所以 Func ✅ 可赋给 Func;真正非法的是反过来)out = 输出(只读、返回值)、in = 输入(只写、参数);违反这个方向就会编译失败。in 和 out 怎么选?不是看“类的继承方向”,而是看泛型参数 T 在接口方法中“出现的位置”:
T 只出现在返回值位置(如 T Get();),用 out T(协变);T 只出现在方法参数位置(如 void Set(T value);),用 in T(逆变);T 同时出现在返回值和参数中(如 T Convert(T input);),那就不能加 in/out——只能是不变(invariant),否则类型系统无法保证安全。IComparer 是 in T,因为 Compare(T x, T y) 两个参数都是输入;IEqualityComparer 同理;而 IComparable 是 in T(CompareTo(T other) 参数是输入),别记反。最易被忽略的一点:协变/逆变只适用于泛型接口和委托,不支持泛型类(如 List)、不支持值类型(int、DateTime 等不能参与 in/out)、也不支持泛型约束中的协变类型(比如 where T : out U 是非法的)。它是一套编译器强约束的“契约”,不是开发者自由发挥的空间。