C#的泛型语法

泛型是OOP语言中三大特征的多态的最重要的体现,协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型。除了协变和逆变,本文也涉及到C#泛型具体语法、原理、泛型约束、泛型缓存等内容。

泛型概述

C# 泛型语法 Since 2.0

using System;
using System.Collections.Generic;

namespace AdvanceCS
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            ShowObject(10);
            ShowObject("zouchanglin");
            object oVal = 1024.0f;
            ShowObject(oVal);

            Console.WriteLine("===============");

            Show<int>(20);
            Show("zouchanglin"); // 尖括号类型可省略
            Show(oVal);

            Console.WriteLine("===============");
            Console.WriteLine(typeof(List<>));
            Console.WriteLine(typeof(Dictionary<,>));
        }

        // 延迟声明类型:把参数类型的声明推迟到调用
        private static void Show<T>(T param)
        {
            Console.WriteLine("This is {0}, param = {1}, type = {2}",
                nameof(Program), param, param.GetType().Name);
        }

        // 早期泛型替代用法
        private static void ShowObject(object obj)
        {
            Console.WriteLine("This is {0}, param = {1}, type = {2}", 
                nameof(Program), obj, obj.GetType().Name);
        }
    }
}

ShowObject这种传递object类型的方式如果是传的值类型会有拆装箱的性能损失,已经不推荐使用了

泛型原理

泛型需要编译器和JIT支持,原来定义的时候就是用了个T作为占位符,起一个模板的作用,我们对其实例化类型参数的时候,补足那个占位符, CLR / JIT根据不同的调用,生成不同的普通方法;泛型不是语法糖,是框架的升级所支持的;

泛型常见用法

普通子类继承泛型类/接口,那么必须指定泛型具体类型;

泛型子类继承泛型类/接口,那么必须指定泛型子类具体类型,泛型类/接口类型从泛型子类获取

/// <summary>
/// 普通类继承泛型抽象类、泛型接口
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericClass<T, T2> : GenericAbsClass<T>, IGenericInterface<T2>
{
    private T _t;

    public GenericClass(T t)
    {
        this._t = t;
    }

    public override T GetT()
    {
        throw new NotImplementedException();
    }

    public void SetT(T2 t)
    {
        throw new NotImplementedException();
    }
}

/// <summary>
/// 泛型接口
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IGenericInterface<T>
{
    void SetT(T t);
}
/// <summary>
/// 泛型抽象类
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class GenericAbsClass<T>
{
    public abstract T GetT();
}

/// <summary>
/// 泛型委托
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
public delegate void demoDelegate<T>(T t);

泛型约束

常见的泛型约束有基类约束、接口约束、引用类型约束、值类型约束、无参构造函数约束、叠加约束

public class Constraint
{
    /// <summary>
    /// 泛型基类约束
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="param"></param>
    public void Show1<T>(T param)
        where T: Person // 泛型基类约束,类似于Java <? extends Type> 
            //  泛型基类约束不能是密封类(sealed)
        {
            param.PrintName();
        }

    /// <summary>
    /// 泛型接口约束,和基类约束一样
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="param"></param>
    public void Show2<T>(T param)
        where T: Work // 泛型接口约束,和基类约束一样
        {
            param.StartWork();
        }

    /// <summary>
    /// 引用类型约束
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="param"></param>
    public void Show3<T>(T param)
        where T : class // 引用类型约束
        {
            param = null;
        }
    /// <summary>
    /// 值类型约束,值类型可用default赋予默认值
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="param"></param>
    public void Show4<T>(T param)
        where T : struct // 值类型约束
        {
            param = default(T); // 根据T的不同赋予默认值
        }

    /// <summary>
    /// 无参构造函数约束, T类型必须有无参构造
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="param"></param>
    public void Show5<T>(T param)
        where T : new() // 无参构造函数约束
        {
            T TNew =  new T();
        }

    /// <summary>
    /// 约束可以叠加,比较灵活
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="param"></param>
    public void Show6<T>(T param)
        where T : Person,Work,new()  // 基类约束 + 接口约束 + 无参构造参数约束
        {
            param.PrintName();

            param.StartWork();
        }

    public static void Main()
    {
        
    }
}

协变和逆变

上面的报错也就是对于编译器来说,虽然Person和Student是父子关系,但是List<Person>List<Student>就不是父子关系,但是正常逻辑来讲的话List<Person>List<Student>是存在父子关系的。

协变就是为了解决这一问题的,这样做其实也是为了解决类型安全问题。

因为协变只能用在接口或者委托类型中,所以我们将List抽象抽来一个空接口 IMyList,然后实现该接口,协变是在T泛型前使用 out 关键字,逆变是在T泛型前使用 in 关键字:

public interface IMyList<in TIn, out TOut>
{
    // 逆变用与参数输入
    void Add(TIn t);

    // 协变用于返回值输出
    TOut Get();
}

public class MyList<TIn, TOut> : IMyList<TIn, TOut>
{
    public void Add(TIn t)
    {
        Console.WriteLine("Add");
    }

    public TOut Get()
    {
        Console.WriteLine("Get");
        return default(TOut);
    }
}


public class Person
{
    public string Name { set; get; }
    public void PrintName()
    {
        Console.WriteLine("name = " + this.Name);
    }
}

public class Student : Person
{
    public string StuId { set; get; }
}


public static void Main()
{
    List<Person> pList1 = new List<Person>();
    //List<Person> sList = new List<Student>(); // 逻辑上是没错的,但是编译器报错
    //List<Person> sList = new List<Student>().Select(c => (Person)c).ToList();

    IMyList<Student, Person> list1 = new MyList<Student, Person>();
    IMyList<Student, Person> list2 = new MyList<Student, Student>(); // 协变
    IMyList<Student, Person> list3 = new MyList<Person, Person>(); // 逆变
    IMyList<Student, Person> list4 = new MyList<Person, Student>(); // 逆变 + 协变
}

协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型。 逆变和协变还有两点:协变时泛型无法作为参数、逆变时泛型无法作为返回值。

类型安全的概念

先想想什么叫做类型安全?一个对象向父类转换时,会隐式安全的转换,而两种不确定可以成功转换的类型(如父类转子类),转换时必须显式转换。解决了类型安全大致就是,这两种类型一定可以转换成功。

协变的意义

协变的话应该很好理解,将子类转换为父类,兼容性好,解决了类型安全(因为子类转父类是肯定可以转换成功的);而协变作为返回值是百分百的类型安全。

逆变的意义

逆变就是将父类泛型隐式转换为子类泛型,其实逆变的内部也是实现子类转换为父类,所以说也是安全的。来看看为什么这么说,其实是观察视角不一样,前者是调用者视角,后者是实现者视角。

下面这个简单的例子也能说明问题:

public interface IListIn<in T>
{
    void Show(T t);
}
public class ListIn<T> : IListIn<T>
{ 
    public void Show(T t)
    { 
        
    }
}

父类接口初始化子类实例时,父类对象无法调用子类方法所以T只能为返回值不能为参数称之为协变:

IListOut<Animal> List = new ListOut<Dog>(); 

子类接口初始化父类实例时,子类对象可以调用父类方法但返回对象未必为父类所以T只能为参数不能为返回值称之为逆变:

IListIn<Dog> List = new ListIn<Animal>();

泛型缓存

这个比较容易理解,JIT每次会把泛型对应的具体类型给实例化之后给缓存下来,不会重新创建新的类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace AdvanceCS
{
    /// <summary>
    /// 每个不同的T,都会生成一份不同的副本
    /// 适合不同类型,需要缓存一份数据的场景,效率高
    /// </summary>
    /// <typeparam name="T"></typeparam>
    class GenericCache<T>
    {
        static GenericCache()
        {
            Console.WriteLine("This is GenericCache static Constructor.");
            _TypeTime = $"{typeof(T)}_{DateTime.Now.ToString()}";
        }

        private static string _TypeTime = "";

        public static string GetCache()
        {
            return _TypeTime;
        }
    }

    public class Test
    {
        public static void Main()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine(GenericCache<int>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache<string>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache<double>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache<float>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache<DateTime>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache<Test>.GetCache());
            }
        }
    }
}