茶余饭后,详谈一下泛型 T

x33g5p2x  于2022-02-07 转载在 其他  
字(4.2k)|赞(0)|评价(0)|浏览(252)

泛型简介

泛型是 .NET 2.0 时出现的产物,C# 2.0 可以使用的语法,它不是语法糖,需要编译器与框架升级共同支持。这一里有个概念“延迟声明”,即把类型的声明延迟到调用。

C# 1.0 时代

有同学会疑问,是不是在 2.0 之前,我们就没有办法用泛型了呢?答案:不是的,在 1.0 时代使用的是 object,接着我通过例子讲解。

下面这段代码,三个方法分别对不同的类型数据进行打印,本质上都在做同一件事情,那可不可以使用一个方法实现呢?答案是:可以的

public static void ShowInt(int val)
{
    Console.WriteLine(val);
}

public static void ShowString(string val)
{
    Console.WriteLine(val);
}

public static void ShowDT(DateTime val)
{
    Console.WriteLine(val);
}

对于上面问题,在 C# 1.0 时代我们可以使用 object 接收其三种类型的参数,然后进行打印。object 类型是一切类型的父类,任何父类出现的地方,都可以用子类代替。请看下面代码

public static void ShowObj(object val)
{
    Console.WriteLine(val);
}

C# 2.0+ 时代

2.0+ 时代,我们可以使用 T 代替 object ,实现不同类型,完成做同一件事情,请看下面代码

public static void ShowMsg<T>(T val)
{
    Console.WriteLine(val);
}

object 与 T

讲过 object 与 T 两种实现方式,有的同学会疑问,既然有了 object 还需要泛型呢?这就涉及到底层 “拆箱” 与 “装箱”。Object 是引用类型,如果传入值类型,就会发生装箱与拆箱转换(先栈 copy 到堆再 copy 到栈,值类型是保存在栈,引用类型保存在堆),这中间会发生性能损耗。而 T 会,将类型的声明延迟到调用的时候,不会发生装箱与拆箱转换,效率远高于 object 方式。

T 如何编译

C# 源代码经过编译器泛型会生成占位符(即 XXX~n),得到 .exe 或者 .dll 。经过 CLR 确定实际类型,会将占位符替换其确定类型,最后生成机器码。占位符不好演示, ~ n 代表有几个泛型。例如 public class Product<T1,T2,T3> 此时经过编译,占位符就是 Product ~3,有兴趣的同学可以通过反编译工具进行查看。

性能测试

这个也是大多数同学关心的问题,首先说结论吧:泛型约等于常规大于 object 。泛型就像是又让马儿跑得快,又让马儿不吃草。我们通过下面例子进行说明,分表有实际类型、object、T 三个方法,进行 1亿次调用

public static void ShowInt(int val)
{
}

public static void ShowObj(object val
{
}

public static void ShowMsg<T>(T val)
{
}
int val = 123;

Stopwatch stopwatchInt = new Stopwatch();
stopwatchInt.Start();
for (int i = 0; i < 100000000; i++)
{
    ShowInt(val);
}
stopwatchInt.Stop();
var intTime = stopwatchInt.ElapsedMilliseconds;

Stopwatch stopwatchObj = new Stopwatch();
stopwatchObj.Start();
for (int i = 0; i < 100000000; i++)
{
    ShowObj(val);
}
stopwatchObj.Stop();
var objTime = stopwatchObj.ElapsedMilliseconds;

Stopwatch stopwatchT = new Stopwatch();
stopwatchT.Start();
for (int i = 0; i < 100000000; i++)
{
    ShowMsg(val);
}
stopwatchT.Stop();
var tTime = stopwatchT.ElapsedMilliseconds;

Console.WriteLine($"int:{intTime};obj:{objTime};T:{tTime}");

可以看到三者分别的耗时,泛型约等于常规大于 object ,所以,泛型在平时可以放心大量使用

泛型类型

泛型也存在许多类型,类、方法、接口、委托,相对来说较为简单,如下:

public class Product<T1,T2,T3>
{
    
}

方法

public T1 GetMst(T1 obj)
{
    // some code

    return obj;
}

接口

public interface Product<T1,T2,T3>
{
    
}

委托

public delegate void GetMsg<T>(T t);

泛型约束

泛型 T 我们也可以进行一些约束,比如限制其类型、基类、引用类型、值类型、无参数构造函数约束等,如下:

基类约束,约束 T 是基类或者基类的子类

public class People
{
    public string Name { get; set; }
}
public class Student:People
{
    public string SchoolNum { get; set; }
}
public static void ShowMsg<T>(T val) where T: People
{

}

接口约束

public static void ShowMsg<T>(T val) where T: IPeopleOption
{

}

引用类型约束

public static void ShowMsg<T>(T val) where T: class
{

}

值类型约束

public static void ShowMsg<T>(T val) where T: struct
{

}

无参数构造函数约束

public static void ShowMsg<T>(T val) where T: new()
{

}

叠加约束

public static void ShowMsg<T>(T val) where T: People, IPeopleOption,new()
{

}

逆变、协变

协变、逆变,是 4.0 出现的东西,就是为了解决那个看似合法,实际不合法的东西,这个东西比较晦涩,给个 demo ,有兴趣的同学可以自己研究一下

public class Bird
{
    public int Id { get; set; }
}

public class Sparrow : Bird
{
    public string Name { get; set; }
}
Bird bird1 = new Bird(); // 鸟是鸟
Bird bird2 = new Sparrow(); // 麻雀是鸟

Sparrow sparrow1 = new Sparrow(); // 麻雀是麻雀
Sparrow sparrow2 = new Bird(); // 鸟而不是麻雀

List<Bird> birds1= new List<Bird>(); // 一堆鸟是一堆鸟
List<Bird> birds2= new List<Sparrow>(); // 一堆麻雀是一堆鸟,报错了为啥呢
List<Bird> birds3 = new List<Sparrow>().Select(c=>(Bird)c).ToList();

{ // 协变
    IEnumerable<Bird> birds1 = new List<Bird>(); // 一堆鸟是一堆鸟
    IEnumerable<Bird> birds2= new List<Sparrow>();// 一堆麻雀是一堆鸟,没报错为啥呢
}

泛型缓存

普通类静态属性,大家都知道,全局就一个,就像单利模式一样。那么泛型类有个静态属性,如果当前泛型类被 int string datetime 调用声明,那这个泛型类的静态属性是共享一份吗?

答案是:不是的。应为泛型在二次编译时,每个不同的 T 都会生成一份不同的副本。即每次接收新 T 会产生一个新类型,当第二次在接受之前接受过的 T 时,共享第一次生成的副本。

通过下面例子来理解,新建一个泛型类,含有一个静态字段用于存储类型消息及声明类型时间,并分表用 int string datetime 两次调用

public class GenericCache<T>
{
    private static string _TypeTime = "";

    static GenericCache()
    {
        _TypeTime = $"{typeof(T)}_{DateTime.Now}";
    }

    public static string GetCache()
    {
        return _TypeTime;
    }
}
Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(1000);
Console.WriteLine(GenericCache<string>.GetCache());
Thread.Sleep(1000);
Console.WriteLine(GenericCache<DateTime>.GetCache());
Thread.Sleep(1000);

Console.WriteLine();

Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCache<string>.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCache<DateTime>.GetCache());

可以看到,第二次调用与第一声明类型时,存储的信息是同样的

扩展:泛型缓存效率远高于字典缓存,泛型是根据类型查找,而字段还需经过哈希

相关文章