C#复习泛型list+协变,逆变
复习
泛型基础,泛型类,泛型接口
namespace _1泛型基础
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
F1(10);
F2("张三");
F3(DateTime.Now);
F4(10);
F4<string>("zs");
Console.WriteLine(Sum(10,20));
Console.WriteLine(Sum1(10, 20.2));
//1 传递的字符串类型
People<string> people = new People<string>();
people.Langue = "字符串类型";
//2 传递一个对象类型 传递一个China类型
People<China> p1 = new People<China>();
p1.Langue = new China();
//3调用有参数的构造函数
People<Japan> p2 = new People<Japan>(new Japan());
Console.WriteLine(p2.Every.Langue);
// 泛型接口的使用
Dog<int> d1 = new Dog<int>();
}
public void F1(int a)
{
Console.WriteLine($"参数的类型{a.GetType().FullName},参数值为:{a}");
}
public void F2(string a)
{
Console.WriteLine($"参数的类型{a.GetType().FullName},参数值为:{a}");
}
// F1和F2只是参数类型不一样,功能都是一样,可以把参数写成object类型,不建议这样操作,主要会产生装箱和拆箱操作
public void F3(object a)
{
Console.WriteLine($"参数的类型{a.GetType().FullName},参数值为:{a}");
}
//定义成泛型方法 在声明的时候不知道T的具体类型, 调用的时候传递具体类型
public void F4<T>(T a )
{
Console.WriteLine($"参数的类型{a.GetType().FullName},参数值为:{a}");
}
//有返回值的泛型方法
public T Sum<T>(T a,T b)
{
//dynamic 动态定义变量
dynamic res = a;
res += b;
//return default (T); // 返回T的默认值
return res;
}
public K Sum1<T,K>(T a, K b)
{
//dynamic 动态定义变量
dynamic res = a;
res += b;
//return default (T); // 返回T的默认值
return res;
}
}
public class China
{
public string Langue { get; set; } = "汉语";
public void Eat( string content)
{
Console.WriteLine(content);
}
}
public class Japan
{
public string Langue { get; set; } = "日语";
public void Eat(string content)
{
Console.WriteLine(content);
}
}
// 泛型类 使用这个类进行定义的时候 传递具体的类型 类型可以是基础类型 也可以是对象类型
public class People<T>
{
public T Langue { get; set; }
public void Eat(T content) // T 只是一个未知类型, 不是泛型方法
{
Console.WriteLine(content);
}
//无参数的构造函数
public People()
{
}
public T Every { get; set; }
public People(T e) //构造函数的参数也可以使用泛型
{
Every = e;
}
}
//泛型接口
public interface IAnimal<T>
{
T Sum(T a, T b);
}
public class Dog<T> : IAnimal<T>
{
public T Sum(T a, T b)
{
throw new NotImplementedException(); //NotImplementedException 未实现的异常
}
}
}
复习
泛型委托
namespace _2泛型委托
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
//内置的委托
Action<int> a1 = a => { Console.WriteLine("无返回值的委托方法"); };
a1(10);
Func<int, int> a2 = a => a * 10;
MessageBox.Show(a2(10)+""); //100
Predicate<int> a3 = a => a % 2 == 0;
MessageBox.Show(a3(10) + ""); //true
//F1(10); 报错
F1("");
F1(new int[] {});
F2(new People());
F2(new Student());
}
// where T : class 引用类型
public void F1<T>(T a) where T : class
{
}
// 基类约束时
public void F2<T>(T a) where T : People
{
}
}
public class People
{
}
public class Student: People
{
}
}
新知识C# 协变与逆变
一、前置基础:普通父子类转换(非泛型)
1. 父子类关系定义
Bird(父类)、Sparrow(子类,麻雀继承鸟类)
public class Bird { }
public class Sparrow : Bird { }
2. 普通类转换规则(必考)
向上转换(隐式转换、安全)
子类对象 → 父类变量,自动转换,不报错
Bird bird = new Bird();
Sparrow sp = new Sparrow();
sp = new Sparrow();
bird = sp; // 子类赋给父类:隐式转换,完全合法
向下转换(显式强制转换、不安全)
父类对象 → 子类变量,直接写报错,必须强制转换
// sp = bird; // 直接赋值报错
sp = (Sparrow)bird; // 强制转换语法通过(运行可能报错)
总结口诀:
子类转父类:自动隐式转换
父类转子类:必须强制转换
二、为什么需要协变逆变?(核心报错原因)
1. 泛型集合不能直接父子转换
List<Bird> birds = new List<Bird>();
List<Sparrow> sparrows = new List<Sparrow>();
// 报错!!!
// birds = sparrows;
2. 报错根本原因(面试必问)
Bird 和 Sparrow 是父子关系
但 List<Bird> 和 List<Sparrow> 没有任何继承关系
List 是密封类,不支持泛型隐式转换,所以直接赋值编译报错。
3. 解决方案:使用支持协变的接口
IEnumerable<out T> 接口自带 out 协变 特性
IEnumerable<Bird> birds1 = new List<Sparrow>();
此时编译通过,这就是协变。
三、协变(Covariance)完整详解
1. 关键字
在T的类型前添加out关键字
2. 转换方向(正向)
子类泛型 → 父类泛型
小范围 → 大范围(和普通类隐式转换方向一致)
3. 适用场景
只能用于:泛型接口、泛型委托
作用:只读、做返回值(out 输出数据)
4. 系统内置协变接口/委托
IEnumerable<out T>
IReadOnlyList<out T>
Func<in T, out TResult>(返回值支持协变)
5. 代码案例:Func 委托协变
// Func<输入参数,返回值>
Func<int, Sparrow> f1 = a => new Sparrow();
// 协变:子类返回值泛型 → 父类返回值泛型
Func<int, Bird> f2 = f1;
解析:
f1 返回 Sparrow(子类)
f2 返回 Bird(父类)
out 支持子类泛型赋值给父类泛型,协变成功。
四、逆变(Contravariance)完整详解
1. 关键字
逆变:在T的类型前添加in关键字
1 in 主要是方法的输入参数进行使用
2. 转换方向(反向)
父类泛型 → 子类泛型
大范围 → 小范围(和普通转换相反)
3. 适用场景
只能用于:泛型接口、泛型委托
作用:只做输入参数(in 接收数据)
4. 系统内置逆变接口/委托
Action<in T>
Predicate<in T>
IComparer<in T>
Func<in T,out TResult>(参数支持逆变)
5. 逆变代码案例1:Predicate
Predicate<Bird> f3 = a => true;
// 逆变:父类泛型 → 子类泛型
Predicate<Sparrow> f4 = f3;
6. 逆变代码案例2:Action
Action<object> f5 = a => { };
// object(父) → Bird(子) 逆变成立
Action<Bird> f6 = f5;
7. 逆变代码案例3:Func参数逆变
Func<Bird, int> f7 = a => 10;
// 参数位置:父类 → 子类 逆变成立
Func<Sparrow, int> f8 = f7;
五、双变共存:Func 同时具备协变+逆变
1. 原理
Func<in T, out TResult>
参数 T:in 逆变(父→子)
返回值 TResult:out 协变(子→父)
所以 Func 是 C# 中唯一一个同时支持协变与逆变的常用委托。
六、协变 & 逆变 终极对照表(必背)
|
特性 |
协变 |
逆变 |
|---|---|---|
|
关键字 |
out |
in |
|
转换方向 |
子类泛型 → 父类泛型 |
父类泛型 → 子类泛型 |
|
读写角色 |
输出、返回值、只读 |
输入、参数、只写 |
|
典型载体 |
IEnumerable、返回值 |
Action、Predicate、参数 |
|
转换趋势 |
向上兼容(同普通隐式转换) |
向下兼容(反向特殊转换) |
七、核心易错点总结(考试必考坑点)
1、List<T> 不支持协变逆变,因为是密封类,只有接口/委托支持
2、out协变:子→父,in逆变:父→子,千万不要记反
3、协变只能用于返回值、只读数据
4、逆变只能用于参数、输入数据
5、Func 双变、Action只有逆变、IEnumerable只有协变
6、普通类有继承关系 ≠ 泛型集合有继承关系
八、极简背诵口诀
out协变,子转父,输出只读向上走
in逆变,父转子,输入参数反向守
集合List不支持,接口委托才拥有
Func双变最特殊,参逆变、返协变
更多推荐


所有评论(0)