c# 协变和逆变是什么

来源:这里教程网 时间:2026-02-21 17:36:29 作者:

协变
逆变
是 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
是非法的)。它是一套编译器强约束的“契约”,不是开发者自由发挥的空间。

相关推荐

热文推荐