泛型是 .NET 2.0 时出现的产物,C# 2.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);
}
2.0+ 时代,我们可以使用 T 代替 object ,实现不同类型,完成做同一件事情,请看下面代码
public static void ShowMsg<T>(T val)
{
Console.WriteLine(val);
}
讲过 object 与 T 两种实现方式,有的同学会疑问,既然有了 object 还需要泛型呢?这就涉及到底层 “拆箱” 与 “装箱”。Object 是引用类型,如果传入值类型,就会发生装箱与拆箱转换(先栈 copy 到堆再 copy 到栈,值类型是保存在栈,引用类型保存在堆),这中间会发生性能损耗。而 T 会,将类型的声明延迟到调用的时候,不会发生装箱与拆箱转换,效率远高于 object 方式。
C# 源代码经过编译器泛型会生成占位符(即 XXX~n),得到 .exe 或者 .dll 。经过 CLR 确定实际类型,会将占位符替换其确定类型,最后生成机器码。占位符不好演示, ~ n 代表有几个泛型。例如 public class Product
这个也是大多数同学关心的问题,首先说结论吧:泛型约等于常规大于 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());
扩展:泛型缓存效率远高于字典缓存,泛型是根据类型查找,而字段还需经过哈希