协变和
逆变是 C# 中让泛型接口和委托支持“安全类型转换”的机制,不是语法糖,也不是运行时魔法——它们由编译器在编译期强制校验,核心目标只有一个:在保持类型安全的前提下,让继承关系能自然地“传导”到泛型参数上。
为什么 IEnumerable<string></string>
能直接赋值给 IEnumerable<object></object>
?
因为
IEnumerable<out t></out>声明了
T是协变的(用
out修饰),意味着:只要
string是
object的子类,那
IEnumerable<string></string>就可隐式转为
IEnumerable<object></object>。这符合直觉——你从集合里“读出来”的东西,子类能当父类用(里氏替换原则)。 ✅ 合法:
IEnumerable<string> strings = new List<string>(); IEnumerable<object> objects = strings; // 协变生效❌ 非法:
IList<string> strings = new List<string>(); IList<object> objects = strings; // 编译错误!IList<T> 不是协变的(因为
IList<t></t>既有
Get又有
Set,无法同时满足协变/逆变约束) ⚠️ 注意:数组也支持协变(如
string[]→
object[]),但它是**不安全的**——运行时可能抛
ArrayTypeMismatchException,而泛型协变是编译期就拦住的,更可靠。
为什么 Action<object></object>
能接收 Action<string></string>
?
因为
Action<in t></in>声明了
T是逆变的(用
in修饰),意思是:你传进去的委托,参数类型越“宽”(越靠继承链顶端),它越能接受“窄”的实际参数。比如一个能处理任意
object的方法,当然也能安全处理
string。 ✅ 合法:
Action<object> actObj = o => Console.WriteLine(o);
Action<string> actStr = actObj; // 逆变生效:string 是 object 的子类
actStr("hello"); // 安全调用,o 接收 string 没问题
❌ 非法:Func<string> funcStr = () => "a"; Func<object> funcObj = funcStr; // 编译错误!Func<T> 的 T 是 out,但 Func<string> 返回 string,不能当 Func<object> 用(返回值太具体)(等等——这里错了?不,
Func<out t></out>是协变的,所以
Func<string></string>✅ 可赋给
Func<object></object>;真正非法的是反过来) ? 关键记忆点:
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<t></t>是
in T,因为
Compare(T x, T y)两个参数都是输入;
IEqualityComparer<t></t>同理;而
IComparable<t></t>是
in T(
CompareTo(T other)参数是输入),别记反。
最易被忽略的一点:协变/逆变只适用于泛型接口和委托,不支持泛型类(如
List<t></t>)、不支持值类型(
int、
DateTime等不能参与 in/out)、也不支持泛型约束中的协变类型(比如
where T : out U是非法的)。它是一套编译器强约束的“契约”,不是开发者自由发挥的空间。
