协变仅适用于接口和委托中声明为out的泛型参数,且t只能出现在输出位置(如返回值、只读属性get访问器),ienumerable和func是典型支持协变的标准接口/委托。

协变(Covariance)不是“能用就行”的语法糖,它只在特定接口/委托中、且类型参数仅出现在输出位置时才生效;试图在类、方法参数或字段里用 out 会直接编译失败。
哪些接口支持协变?看标准库里的 IEnumerable<t></t> 和 Func<tresult></tresult>
协变必须由接口或委托显式声明 out 才能启用。.NET 基础类库里最典型的例子是:
-
IEnumerable<t></t>:定义为interface IEnumerable<out t></out>,所以IEnumerable<string></string>可隐式转成IEnumerable<object></object> -
Func<tresult></tresult>:定义为delegate TResult Func<out tresult>();</out>,所以Func<dog></dog>能赋给Func<animal></animal>
注意:IList<t></t> 不支持协变——它没加 out,而且含 set 和 Add 等输入操作,违反协变约束。
自己定义协变接口时,out T 的使用边界在哪?
声明 interface IProducer<out t></out> 后,T 只能出现在:
- 方法返回值类型(如
T Get();) - 只读属性的
get访问器(如T Value { get; }) - 索引器的返回类型(如
T this[int i] { get; })
以下写法全部非法,编译器会报错:
-
void Set(T value);(T出现在参数位) -
T Field;(字段不允许) -
T Property { get; set; }(set访问器引入输入) -
void Process(List<t> items);</t>(泛型类型嵌套后,T在List<t></t>内部仍算“输入位置”)
为什么泛型类不能加 out 或 in?
因为类天然同时承担“生产”和“消费”角色:
- 构造函数参数、方法参数 → 输入位置
- 属性
get、返回值、只读字段 → 输出位置
无法统一满足 out(只许输出)或 in(只许输入)的语义约束。哪怕你只写 get 属性,编译器也无法保证未来不加 set,所以直接禁止泛型类使用变体修饰符。
常见误操作:给 class Repository<t></t> 加 where T : class 并幻想它能协变——没用,语法层就拒绝解析。
协变的实际陷阱:值类型不参与协变
out 仅对引用类型有效。下面代码会编译失败:
IEnumerable<int> ints = new List<int>(); IEnumerable<object> objects = ints; // ❌ 编译错误 </object></int></int>
原因:值类型没有继承关系,int 不是 object 的子类(装箱是运行时行为,不构成类型系统中的子类型)。协变依赖的是静态可验证的继承链,所以只有 string→object、Dog→Animal 这类引用类型转换才成立。











