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}