.NET进阶篇-语言章-1-Generic泛型深入

2019-10-08

内容目录

一、概述二、泛型的好处三、泛型使用1、泛型方法2、泛型类、泛型接口四、泛型的功能1、泛型中的默认值2、约束3、协变逆变5、泛型委托4、泛型缓存五、总结

一、概述

泛型我们一定都用过,最常见的List<T>集合。.NET2.0开始支持泛型,创建的目的就是为了不同类型创建相同的方法或类,也包括接口,委托的泛型。比如常见的ORM映射,一个方法通过传入不同的类,返回不同的类实例,再调用时才确定参数类型。

我们知道想要一个类相同名称的方法,如果仅参数类型不同,那么要重载。重载会有很多冗余的代码。在.NET1.0时代也可以不用重载,那就是参数类型直接用Object类型,那么任何类型都能传进去了,但是会有装箱拆箱操作,影响性能。

public static void Show(string sValue)
{
    Console.WriteLine(sValue);
}

public static void Show(int iValue)
{
    Console.WriteLine(iValue);
}

public static void Show(object oValue)
{
    Console.WriteLine(oValue);
}

二、泛型的好处

值类型和引用类型的装箱拆箱消耗。值类型分配在线程栈上,引用类型分配在堆上,只把指针放在栈上。如图所示,如果把int类型1装箱,就要把1拷贝到堆中,就会有内存的交换。以前的ArrayList就是类型不安全的,需要频繁的进行装拆箱操作,Add元素的时候全部装箱object,取的时候要拆箱,性能损失比较大。

泛型的效率等同于硬编码的方式,就是和你很多功能相同的类效率差不多。泛型每个类型只实例化一次,下面泛型缓存会详细解读下。先简单介绍下CLR的运行原理(详细在CLR章节)以了解泛型的原理机制。

.NET编译器和解释器两阶段,我们先经过编译器编译成IL中间语言(dll、exe),和java的字节码类似,然后经过JIT解释成机器码。这样做的好处就是我们只需要编译成IL后,在各个不同计算机系统上,只要有对应的CLR(JIT)就行,这样就和平台无关。二次编译:为了一次编译,不同平台使用。泛型在第一个编译时会用一个占位符代替,在第二次运行时会编译成具体的类型。所以性能相当于硬编码的方式,每种类型最终都有自己的机器码。

List<T>是在使用时定义类型,JIT编译器解析时动态的生成,如定义List<int>,在JIT运行时就声称List<int>类型,然后操作就不会出现装箱拆箱,而且只能添加指定的类型,这就类型安全

三、泛型使用

1、泛型方法

常见的泛型方法就是在方法后面带上<T>(T param),“T”可以随便定义,只要不是关键保留字就行,默认约定俗成都用T,此处就代表你定义了一个T类,然后后面参数就可以用这个T类型。(如果把鼠标光标放在参数类型T上,然后F12转到定义就会定位到前面这个T。)这样就可以用一个方法,满足不同的参数类型,去做相同的事情。把参数的类型申明推迟到调用时,延迟声明。后面框架中也会有很多这种延迟思想,延迟以达到更好的扩展。

public static void Show<T>(T tValue)
{
    Console.WriteLine(tValue);
}
CommonMethod.Show<int>(123);
2、泛型类、泛型接口

创建方法类似,语法一样<T>。用的最多的List<T>就是很典型的泛型类,用来满足不同的具体类型,完成相同的事情

public class GenericClass<T>
{
    public T _T;
}

public interface IGenericInterface<T>
{
    GetT();
}

四、泛型的功能

1、泛型中的默认值

既然用了泛型,那么在内部想要初始化怎么办呢?因为泛型进来的类型不一定是值类型或引用类型,所以初始化就不能简单直接赋null。这个时候需要用到default关键字,用于将泛型类型初始化为null或其他值类型默认值(0,0001/1/1 0:00:00日期等);

2、约束

泛型导致任何类型都可以进来,那么如何去使用这个类型T,编写的时候我们是不知道T是什么,也不知道它能干什么。一个方法就是可以用反射,任何一个类型通过发射都能获取内部的结构属性方法调用。泛型约束提供更简便的方法。在声明泛型时在参数后面追加where关键字。约束可以同时指定多个,像这样where:T People,IWork,new()。同时约束传进来的类型People或其子类,并且继承了IWork接口,有无参数构造函数。

public static void Show<T>(T tValue) where T : People
{
    Console.WriteLine(tValue.Name);
}
3、协变逆变

协变逆变就是对参数和返回值的类型进行转换。协变用一个派生更大的类去代替某个类型(小代替大),其实就是设计原则的里氏替换原则,比如狗继承自动物,那么任何用动物作为参数类型的地方,调用时都可以用狗代替。逆变就是反过来。

//协变
public void ShowName(Animal animal)
{
}

ShowName(dog);

泛型接口的协变逆变。如果泛型类型用了out关键字标注,泛型接口就是协变的。这也意味着返回类型只能是T。如果用了in关键字标注,就是逆变,只能把泛型类型T用作方法的输入。这块很绕,实际使用非常少。

//一堆狗肯定是一堆动物啊,为啥就不能这么做呢?下面这句编译不通过
//前后两个类型是没有父子关系的
List<Animal> animalLst = new List<Dog>();
//下面这句就可以呢?
IEnumerable<Animal> animalLst2 = new List<Dog>();

//因为在接口中添加了out关键字
public interface IEnumerable<out T> : IEnumerable
{
    //
    // 摘要:
    //     Returns an enumerator that iterates through the collection.
    //
    // 返回结果:
    //     An enumerator that can be used to iterate through the collection.
    IEnumerator<T> GetEnumerator();
}

ICustomListIn<Dog> customLstIn = new CustomListIn<Animal>();

public interface ICustomListIn<in T>
{
    void Show(T t);
}

public class CustomListIn<T> : ICustomListIn<T>
{
    public void Show(T t)
    {
        Console.WriteLine(typeof(T).FullName);
    }
}

interface ISetData<in T>  //使用逆变
{
    void SetData(T data);
}

interface IGetData<out T>   //使用协变
{
    T GetData();
}

class MyTest<T> : ISetData<T>, IGetData<T>//继承两个泛型接口
{
    private T data;
    public void SetData(T data)
    {
        this.data = data;   //赋值
    }
    public T GetData()
    {
        return this.data;   //取数据
    }
}

MyTest<object> my = new MyTest<object>();
ISetData<string> set = my;
set.SetData("nihao");

其实协变逆变就是语法糖,为了让不是继承关系的类型也可以互相赋值编译通过。运行时实际右边是什么类型就是什么类型。(欺骗编译器,自己应该会很少写协变逆变的接口或委托)

5、泛型委托

以Action为例,Action是.NET Framework内置的泛型委托,可以使用Action委托以参数形式传递方法,而不用显示声明自定义的委托。其实我们撸代码过程中不太需要自己定义委托,内置的Action和Func就够用,也便于统一。Action无返回值委托,可以有16个参数,可以传入不同的类型。在委托事件一章会详细介绍。

4、泛型缓存

泛型类的静态成员只能在类的一个实例中共享。运行时泛型类的实例已经指定了具体类型,每一个不同的泛型类实例共享静态成员,利用这个特点就可以做缓存。每一个不同的T缓存一个版本数据。如例子所示,当第一次指定不同的T时,会重新构造,再次有相同的类型时,就不会进入静态构造函数了。相当于为缓存了多个版本的静态成员。比如在各个数据库实体类需要有一些增删改查的SQL时,就可以利用用泛型特性,每一个数据库实体类都会缓存一份自己的增删改查SQL。

public class GenericCache<T>
{

    static GenericCache()
    
{
        Console.WriteLine("进入静态构造函数");
        _TypeTime = $"{typeof(T).FullName}_{DateTime.Now.ToString()}";
    }

    private static string _TypeTime = "";

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

Console.WriteLine("************************");
Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(1000);
Console.WriteLine(GenericCache<string>.GetCache());
Thread.Sleep(1000);
Console.WriteLine("认真比较打印出的静态成员值");
Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(1000);
Console.WriteLine(GenericCache<string>.GetCache());
Console.WriteLine("************************");

五、总结

通过泛型类可以创建独立于类型的类,泛型方法创建出独立于类型的方法。接口、结构、委托也可以用泛型的方式创建。建议如果我们需要设计和类型无关的对象时,可以使用泛型,把锅甩给调用方,由上端决定实例化具体什么类型。