协变(out)和逆变(in)仅适用于接口和委托,协变允许更具体的类型赋给更宽泛的类型变量,仅限返回值、get访问器等输出位置;逆变则用于输入位置如方法参数,仅限接口和委托;类不支持协变或逆变。

协变(out)只适用于接口和委托,且只能用在输出位置
协变允许你把更具体的类型赋给更宽泛的类型变量,比如 IEnumerable<string> 能赋给 IEnumerable<object>。但这不是所有泛型都支持——只有标记了 out 的类型参数才允许协变,而且它**只能出现在返回值、属性 get 访问器、方法返回类型中**,不能出现在参数、字段或 set 访问器里。
常见错误现象:void Add(T item) 里用了 out T,编译直接报错 CS1961:“类型参数 T 具有无效的方差”。
-
IEnumerable<T>、IComparer<out T>、Func<out T>是典型协变接口/委托 - 自定义接口要协变,必须显式写
interface IProducer<out T>,且所有T出现位置都得是“只读”的 - 类不支持协变或逆变——
class List<out T>是非法语法
逆变(in)只用于输入位置,且仅限接口和委托
逆变让你把更宽泛的类型传给更具体的期望,比如 Action<object> 可以赋给 Action<string>(因为 string 是 object 的子类,而 Action 只消费参数,不返回它)。逆变的关键是:类型参数只能出现在**参数位置**,不能用于返回值或属性 get。
使用场景:当你需要一个“能处理任意父类的委托”,但实际只想传子类实例时,逆变就省去了手动包装。
-
Action<in T>、IComparer<in T>、Predicate<in T>是典型逆变接口/委托 - 错误写法:
T GetFirst()出现在interface IConsumer<in T>里 → 编译失败CS1961 - 注意
IComparer<T>同时支持in和out?不对。.NET 中它是IComparer<in T>,因为比较逻辑只接收两个T参数,不返回T
泛型类永远不支持 in/out,别试了
你没法给 class Box<T> 加 in 或 out。C# 语言规范明确禁止——类的类型参数必须是不变的(invariant)。这是因为类既有输入(构造函数、方法参数),又有输出(属性、返回值),无法统一满足协变或逆变约束。
常见错误现象:写了 class Repository<out T>,VS 立刻红波浪线,错误信息是 CS0266 或更直接的 CS1961。
- 想“模拟”协变?可以暴露一个协变接口,比如让
Repository<User>实现IQueryable<out User>(但IQueryable<T>本身是协变的) - 别试图用
where T : class或其他约束绕过 in/out 限制——没用,语法层就卡死 - 如果真需要运行时类型弹性,考虑用非泛型基类 +
object或dynamic,但会丢掉编译期检查
协变逆变不是类型转换,而是接口/委托的引用兼容性
协变逆变不会改变对象实际类型,也不触发装箱/拆箱或隐式转换。它只是告诉编译器:“这个接口引用,我允许你用更宽或更窄的泛型实参来持有它”。底层对象还是原来那个,只是引用视角变了。
性能影响几乎为零——没有额外开销,纯编译期检查。但兼容性要注意:.NET Framework 4+ 和 .NET Core / 5+ 都支持,但旧版 Mono 或某些 AOT 场景可能有细微差异。
- 错误理解:“把
List<string>赋给IEnumerable<object>就是协变” → 不对,List<T>本身不变,但它的IEnumerable<T>接口实现是协变的 - 协变失效的典型:试图把
IList<string>赋给IList<object>→ 编译失败,因为IList<T>是不变的(含Add(T)方法) - 逆变也一样:不能把
Func<string, bool>当作Func<object, bool>用,因为Func<in T, out TResult>的第一个参数是in,但第二个是out,整体不满足纯逆变
实际用的时候,最常踩的坑是盯着具体类猛加 in/out,结果编译不过;或者以为协变能让集合“安全向上转型”,却忘了 IList<T> 这种可写的接口根本不支持。记住:只看接口/委托声明,只看参数位置,其它都是干扰。


















