一、基础:普通类的父子转换(铺垫)
1. 隐式转换(向上转型)
子类对象可以直接赋值给父类变量(隐式转换、安全)
cs
Bird bird = new Bird();
Sparrow sp = new Sparrow();
sp = new Sparrow();
bird = sp; // 隐式转换:子类 → 父类(小范围 → 大范围,安全)
2. 显式强制转换(向下转型)
父类对象不能直接赋值给子类变量,必须强制转换
cs
// sp = bird; // 直接报错:父类不能隐式转子类
sp = (Sparrow)bird; // 强制转换:父类 → 子类
总结
子类 → 父类:隐式转换(天然支持)
父类 → 子类:必须强制转换
二、核心难点:List<T> 为什么不支持转换?
代码报错原因(必考)
cs
List<Bird> birds = new List<Bird>();
List<Sparrow> sparrows = new List<Sparrow>();
// birds = sparrows; 【编译报错】
原理
Bird 和 Sparrow 是父子关系,但是 List<Bird> 和 List<Sparrow> 没有任何继承关系!
List 是普通泛型类,不具备协变/逆变能力,所以无法互相赋值。
三、协变(out T)------ 子类泛型 → 父类泛型
1. 协变关键字:out
2. 协变规则(三条铁律)
-
转换方向:子类泛型 → 父类泛型(正向、向上)
-
只能用于:泛型接口 / 泛型委托
-
语法:泛型参数前加 out T
3. 协变经典案例(IEnumerable<out T>)
cs
// 支持协变:子类集合 → 父类集合
IEnumerable<Bird> birds1 = new List<Sparrow>();
原因:IEnumerable<out T> 接口 T 带 out 协变标识。
4. 委托协变案例(Func 返回值协变)
cs
// Func<in T, out TResult> 返回值支持协变
Func<int, Sparrow> f1 = a => new Sparrow();
Func<int, Bird> f2 = f1;
// 协变:Sparrow(子类) 返回值 → Bird(父类) 返回值
5. 常见支持协变的系统接口/委托
-
IEnumerable<out T>
-
IReadOnlyList<out T>
-
IReadOnlyCollection<out T>
-
Func<out TResult>
四、逆变(in T)------ 父类泛型 → 子类泛型
1. 逆变关键字:in
2. 逆变规则(三条铁律)
-
转换方向:父类泛型 → 子类泛型(反向、向下)
-
只能用于:泛型接口 / 泛型委托
-
作用于:方法输入参数
3. 逆变全套案例
cs
// 1. Predicate 委托逆变
Predicate<Bird> f3 = a => true;
Predicate<Sparrow> f4 = f3;
// 逆变:父类泛型 → 子类泛型
// 2. Action 委托逆变
Action<object> f5 = a => { };
Action<Bird> f6 = f5;
// 3. Func 参数逆变
Func<Bird, int> f7 = a => 10;
Func<Sparrow, int> f8 = f7;
4. 常见支持逆变的系统接口/委托
-
Action<in T>
-
Predicate<in T>
-
IComparer<in T>
-
IEqualityComparer<in T>
五、既协变又逆变:Func<in T, out TResult>
Func 委托同时拥有两种特性:
-
参数 in T:支持逆变(父参数 → 子参数)
-
返回值 out TResult:支持协变(子返回值 → 父返回值)
万能双向转换委托!
六、最终总结对照表(必背)
| 类型 | 关键字 | 转换方向 | 适用位置 |
|---|---|---|---|
| 协变 | out | 子类泛型 → 父类泛型 | 返回值、集合读取 |
| 逆变 | in | 父类泛型 → 子类泛型 | 方法输入参数 |
七、完整可运行基类代码
cs
public class Bird
{
}
public class Sparrow : Bird
{
}
八、口诀(秒杀面试题)
out协变,子转父;
in逆变,父转子;
List普通不转换,接口委托才支持;
参数in来返回out,双向Func最省事。