C#的泛型语法
泛型是OOP语言中三大特征的多态的最重要的体现,协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型。除了协变和逆变,本文也涉及到C#泛型具体语法、原理、泛型约束、泛型缓存等内容。
泛型概述
C# 泛型语法 Since 2.0
1using System;
2using System.Collections.Generic;
3
4namespace AdvanceCS
5{
6 public class Program
7 {
8 public static void Main(string[] args)
9 {
10 Console.WriteLine("Hello World!");
11
12 ShowObject(10);
13 ShowObject("zouchanglin");
14 object oVal = 1024.0f;
15 ShowObject(oVal);
16
17 Console.WriteLine("===============");
18
19 Show<int>(20);
20 Show("zouchanglin"); // 尖括号类型可省略
21 Show(oVal);
22
23 Console.WriteLine("===============");
24 Console.WriteLine(typeof(List<>));
25 Console.WriteLine(typeof(Dictionary<,>));
26 }
27
28 // 延迟声明类型:把参数类型的声明推迟到调用
29 private static void Show<T>(T param)
30 {
31 Console.WriteLine("This is {0}, param = {1}, type = {2}",
32 nameof(Program), param, param.GetType().Name);
33 }
34
35 // 早期泛型替代用法
36 private static void ShowObject(object obj)
37 {
38 Console.WriteLine("This is {0}, param = {1}, type = {2}",
39 nameof(Program), obj, obj.GetType().Name);
40 }
41 }
42}
ShowObject这种传递object类型的方式如果是传的值类型会有拆装箱的性能损失,已经不推荐使用了
泛型原理
泛型需要编译器和JIT支持,原来定义的时候就是用了个T
作为占位符,起一个模板的作用,我们对其实例化类型参数的时候,补足那个占位符, CLR
/ JIT
根据不同的调用,生成不同的普通方法;泛型不是语法糖,是框架的升级所支持的;
泛型常见用法
普通子类继承泛型类/接口,那么必须指定泛型具体类型;
泛型子类继承泛型类/接口,那么必须指定泛型子类具体类型,泛型类/接口类型从泛型子类获取
1/// <summary>
2/// 普通类继承泛型抽象类、泛型接口
3/// </summary>
4/// <typeparam name="T"></typeparam>
5public class GenericClass<T, T2> : GenericAbsClass<T>, IGenericInterface<T2>
6{
7 private T _t;
8
9 public GenericClass(T t)
10 {
11 this._t = t;
12 }
13
14 public override T GetT()
15 {
16 throw new NotImplementedException();
17 }
18
19 public void SetT(T2 t)
20 {
21 throw new NotImplementedException();
22 }
23}
24
25/// <summary>
26/// 泛型接口
27/// </summary>
28/// <typeparam name="T"></typeparam>
29public interface IGenericInterface<T>
30{
31 void SetT(T t);
32}
33/// <summary>
34/// 泛型抽象类
35/// </summary>
36/// <typeparam name="T"></typeparam>
37public abstract class GenericAbsClass<T>
38{
39 public abstract T GetT();
40}
41
42/// <summary>
43/// 泛型委托
44/// </summary>
45/// <typeparam name="T"></typeparam>
46/// <param name="t"></param>
47public delegate void demoDelegate<T>(T t);
泛型约束
常见的泛型约束有基类约束、接口约束、引用类型约束、值类型约束、无参构造函数约束、叠加约束
1public class Constraint
2{
3 /// <summary>
4 /// 泛型基类约束
5 /// </summary>
6 /// <typeparam name="T"></typeparam>
7 /// <param name="param"></param>
8 public void Show1<T>(T param)
9 where T: Person // 泛型基类约束,类似于Java <? extends Type>
10 // 泛型基类约束不能是密封类(sealed)
11 {
12 param.PrintName();
13 }
14
15 /// <summary>
16 /// 泛型接口约束,和基类约束一样
17 /// </summary>
18 /// <typeparam name="T"></typeparam>
19 /// <param name="param"></param>
20 public void Show2<T>(T param)
21 where T: Work // 泛型接口约束,和基类约束一样
22 {
23 param.StartWork();
24 }
25
26 /// <summary>
27 /// 引用类型约束
28 /// </summary>
29 /// <typeparam name="T"></typeparam>
30 /// <param name="param"></param>
31 public void Show3<T>(T param)
32 where T : class // 引用类型约束
33 {
34 param = null;
35 }
36 /// <summary>
37 /// 值类型约束,值类型可用default赋予默认值
38 /// </summary>
39 /// <typeparam name="T"></typeparam>
40 /// <param name="param"></param>
41 public void Show4<T>(T param)
42 where T : struct // 值类型约束
43 {
44 param = default(T); // 根据T的不同赋予默认值
45 }
46
47 /// <summary>
48 /// 无参构造函数约束, T类型必须有无参构造
49 /// </summary>
50 /// <typeparam name="T"></typeparam>
51 /// <param name="param"></param>
52 public void Show5<T>(T param)
53 where T : new() // 无参构造函数约束
54 {
55 T TNew = new T();
56 }
57
58 /// <summary>
59 /// 约束可以叠加,比较灵活
60 /// </summary>
61 /// <typeparam name="T"></typeparam>
62 /// <param name="param"></param>
63 public void Show6<T>(T param)
64 where T : Person,Work,new() // 基类约束 + 接口约束 + 无参构造参数约束
65 {
66 param.PrintName();
67
68 param.StartWork();
69 }
70
71 public static void Main()
72 {
73
74 }
75}
协变和逆变
上面的报错也就是对于编译器来说,虽然Person和Student是父子关系,但是List<Person>
和List<Student>
就不是父子关系,但是正常逻辑来讲的话List<Person>
和List<Student>
是存在父子关系的。
协变就是为了解决这一问题的,这样做其实也是为了解决类型安全问题。
因为协变只能用在接口或者委托类型中,所以我们将List抽象抽来一个空接口 IMyList,然后实现该接口,协变是在T泛型前使用 out
关键字,逆变是在T泛型前使用 in
关键字:
1public interface IMyList<in TIn, out TOut>
2{
3 // 逆变用与参数输入
4 void Add(TIn t);
5
6 // 协变用于返回值输出
7 TOut Get();
8}
9
10public class MyList<TIn, TOut> : IMyList<TIn, TOut>
11{
12 public void Add(TIn t)
13 {
14 Console.WriteLine("Add");
15 }
16
17 public TOut Get()
18 {
19 Console.WriteLine("Get");
20 return default(TOut);
21 }
22}
23
24
25public class Person
26{
27 public string Name { set; get; }
28 public void PrintName()
29 {
30 Console.WriteLine("name = " + this.Name);
31 }
32}
33
34public class Student : Person
35{
36 public string StuId { set; get; }
37}
38
39
40public static void Main()
41{
42 List<Person> pList1 = new List<Person>();
43 //List<Person> sList = new List<Student>(); // 逻辑上是没错的,但是编译器报错
44 //List<Person> sList = new List<Student>().Select(c => (Person)c).ToList();
45
46 IMyList<Student, Person> list1 = new MyList<Student, Person>();
47 IMyList<Student, Person> list2 = new MyList<Student, Student>(); // 协变
48 IMyList<Student, Person> list3 = new MyList<Person, Person>(); // 逆变
49 IMyList<Student, Person> list4 = new MyList<Person, Student>(); // 逆变 + 协变
50}
协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型。 逆变和协变还有两点:协变时泛型无法作为参数、逆变时泛型无法作为返回值。
类型安全的概念
先想想什么叫做类型安全?一个对象向父类转换时,会隐式安全的转换,而两种不确定可以成功转换的类型(如父类转子类),转换时必须显式转换。解决了类型安全大致就是,这两种类型一定可以转换成功。
协变的意义
协变的话应该很好理解,将子类转换为父类,兼容性好,解决了类型安全(因为子类转父类是肯定可以转换成功的);而协变作为返回值是百分百的类型安全。
逆变的意义
逆变就是将父类泛型隐式转换为子类泛型,其实逆变的内部也是实现子类转换为父类,所以说也是安全的。来看看为什么这么说,其实是观察视角不一样,前者是调用者视角,后者是实现者视角。
下面这个简单的例子也能说明问题:
1public interface IListIn<in T>
2{
3 void Show(T t);
4}
5public class ListIn<T> : IListIn<T>
6{
7 public void Show(T t)
8 {
9
10 }
11}
父类接口初始化子类实例时,父类对象无法调用子类方法所以T只能为返回值不能为参数称之为协变:
1IListOut<Animal> List = new ListOut<Dog>();
子类接口初始化父类实例时,子类对象可以调用父类方法但返回对象未必为父类所以T只能为参数不能为返回值称之为逆变:
1IListIn<Dog> List = new ListIn<Animal>();
泛型缓存
这个比较容易理解,JIT每次会把泛型对应的具体类型给实例化之后给缓存下来,不会重新创建新的类。
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Text;
5using System.Threading;
6using System.Threading.Tasks;
7
8namespace AdvanceCS
9{
10 /// <summary>
11 /// 每个不同的T,都会生成一份不同的副本
12 /// 适合不同类型,需要缓存一份数据的场景,效率高
13 /// </summary>
14 /// <typeparam name="T"></typeparam>
15 class GenericCache<T>
16 {
17 static GenericCache()
18 {
19 Console.WriteLine("This is GenericCache static Constructor.");
20 _TypeTime = $"{typeof(T)}_{DateTime.Now.ToString()}";
21 }
22
23 private static string _TypeTime = "";
24
25 public static string GetCache()
26 {
27 return _TypeTime;
28 }
29 }
30
31 public class Test
32 {
33 public static void Main()
34 {
35 for (int i = 0; i < 5; i++)
36 {
37 Console.WriteLine(GenericCache<int>.GetCache());
38 Thread.Sleep(10);
39 Console.WriteLine(GenericCache<string>.GetCache());
40 Thread.Sleep(10);
41 Console.WriteLine(GenericCache<double>.GetCache());
42 Thread.Sleep(10);
43 Console.WriteLine(GenericCache<float>.GetCache());
44 Thread.Sleep(10);
45 Console.WriteLine(GenericCache<DateTime>.GetCache());
46 Thread.Sleep(10);
47 Console.WriteLine(GenericCache<Test>.GetCache());
48 }
49 }
50 }
51}