协变out用于只含输出位置(如返回值、只读属性)的泛型接口或委托,如ienumerable、func;逆变in用于只含输入位置(如方法参数)的泛型接口或委托,如icomparer、action。

协变 out 用在什么接口/委托上?
协变允许你用更具体的类型替换泛型参数,但前提是该参数只作为返回值出现。所以 out 只能用于输出位置:接口或委托的泛型参数如果只出现在返回类型中(比如方法返回值、只读属性的 get 访问器),才能加 out。
常见可协变的接口有 IEnumerable<t></t>、IReadOnlyList<t></t>、Func<t></t>(无参返回 T 的委托)。例如:
IEnumerable<string> strings = new List<string>(); IEnumerable<object> objects = strings; // ✅ 合法:string 是 object 的子类,且 IEnumerable<T> 的 T 是 out
但不能用于 List<t></t> 这种可变集合——它有 Add(T item),T 出现在输入位置,不满足协变条件。
逆变 in 为什么只能用于输入参数?
逆变允许你用更通用的类型替换泛型参数,但它要求该参数只作为输入参数出现。所以 in 只能用于方法参数、委托参数等“进来的”位置。
典型例子是 IComparer<t></t> 和 Action<t></t>:
IComparer<object> comparer = Comparer<object>.Default; IComparer<string> stringComparer = comparer; // ✅ 合法:string 是 object 的子类,且 IComparer<T> 的 T 是 in
因为 Compare(T x, T y) 中的 T 只用来接收参数,不返回 T,所以可以安全地“向上兼容”。
注意:一个泛型参数不能同时是 in 和 out;也不能在同一个接口里既当输入又当输出(否则编译器会报错)。
自定义接口加 in 或 out 的硬性限制
你不能随便给任意泛型接口加 in 或 out,编译器会严格检查所有成员中的使用位置:
- 加
out T→ 所有T必须出现在仅输出位置:返回类型、只读属性、委托返回值等 - 加
in T→ 所有T必须出现在仅输入位置:方法参数、委托参数、ref/out参数的类型不允许(它们是双向的,违反纯输入)
例如这个接口无法标记为 out:
interface Bad<out T> {
T Get(); // ✅ 输出
void Set(T value); // ❌ 编译错误:T 出现在输入位置
}委托中 Func 和 Action 的 in/out 已经预设好了
不必自己写 delegate 声明,.NET 内置委托已经按规则标注:
-
Func<tresult></tresult>:TResult 是out -
Func<t tresult></t>:T 是in,TResult 是out -
Action<t></t>:T 是in -
Predicate<t></t>:T 是in -
Comparison<t></t>:T 是in
这意味着你可以直接利用这些协变/逆变能力做类型转换,但别试图把 Action<string></string> 赋给 Action<object></object>——那是反的,会编译失败;正确的是 Action<object> action = x => Console.WriteLine(x); Action<string> stringAction = action;</string></object>(因为 string 可安全传给期望 object 的地方)。
真正容易被忽略的是:协变和逆变只对引用类型生效。如果你用 int、struct,哪怕满足位置要求,也无法进行这种隐式转换——编译器不会报错,但转换语句根本不会通过类型检查。









