C#泛型详解,作者.NET开发菜鸟

这篇文章主要讲解C#中的泛型,泛型在C#中有很重要的地位,尤其是在搭建项目框架的时候。

一、什么是泛型

  1. 泛型是C#2.0退出的新语法,不是语法糖,而是2.0由框架升级提供的功能。<br /> 我们在编程的时候,经常会遇到功能非常相似的模块,只是它们处理的数据不一样。但我们没有办法,只能分别写多个方法来处理不同的数据类型。这个时候,那么问题来了,有没有一种办法,用同一个方法来处理传入不同种类型参数的办法呢?泛型的出现就是专门来解决这个问题的。

二、为什么使用泛型

例子:

  1. public class CommonMethod
  2. {
  3. /// <summary>
  4. /// 打印个int值
  5. ///
  6. /// 因为方法声明的时候,写死了参数类型
  7. /// 已婚的男人 Eleven San
  8. /// </summary>
  9. /// <param name="iParameter"></param>
  10. public static void ShowInt(int iPatamenter)
  11. {
  12. Console.WriteLine("This
  13. is{0},parameter={1},type={2}",
  14. typeof(CommonMethod).Name,
  15. iPatamenter.GetType().Name, iPatamenter);
  16. }
  17. /// <summary>
  18. /// 打印个string值
  19. /// </summary>
  20. /// <param name="sParameter"></param>
  21. public static void ShowString(string sParameter)
  22. {
  23. Console.WriteLine("This is
  24. {0},parameter={1},type={2}",
  25. typeof(CommonMethod).Name,
  26. sParameter.GetType().Name, sParameter);
  27. }
  28. /// <summary>
  29. /// 打印个DateTime值
  30. /// </summary>
  31. /// <param name="oParameter"></param>
  32. public static void ShowDateTime(DateTime dtParameter)
  33. {
  34. Console.WriteLine("This is
  35. {0},parameter={1},type={2}",
  36. typeof(CommonMethod).Name,
  37. dtParameter.GetType().Name, dtParameter);
  38. }
  39. }

结果:
未命名图片.png

从上面的结果中我们可以看出这三个方法,除了传入的参数不同外,其里面实现的功能都是一样的。在1.0版的时候,还没有泛型这个概念,那么怎么办呢。相信很多人会想到OOP(面向对象编程(object-oriented programming))三大特性之一的继承,我们知道,C#语言中,object是所有类型的基类,将上面的代码进行以下优化:

  1. public static void ShowObjec(object oParameter)
  2. {
  3. Console.WriteLine("this is
  4. {0},parameter={1},type={2}",
  5. typeof(CommonMethod).Name,oParameter.GetType().Name,oParameter);
  6. }

结果:
未命名图片.png
从上面的结果中我们可以看出,使用Object类型达到了我们的要求,解决了代码的可复用性。可能有人会问定义的是object类型的,为什么可以传入int、string、等类型呢?原因有二:
1.object类型是一切类型的父类
2.通过继承,子类拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替。
但是上面的object类型的方法又会带来另外一个问题:装箱和拆箱,会损耗程序的性能。
微软在C#2.0的时候推出了泛型,可以很好的解决上面的问题。

三、泛型类型参数

  1. 在泛型类型或方法定义中,类型参数是在其实例化泛型类型的一个变量时,客户端指定的特定类型的占位符。泛型类(GenericList<T>)无法按原样使用,因为它不是真正的类型;它更像是类型的蓝图。若要使用GenericList<T>,客户端代码必须通过指定尖括号内的类型参数来声明并实例化构造类型。此特定类的类型参数可以使编译器可识别的任何类型。可创建任意数量的构造类型实例,其中每个使用不同的类型参数。<br /> 上面例子代码修改为泛型,如下:
  1. public static void Show<T>(T TParameter)
  2. {
  3. Console.WriteLine("This is
  4. {0},parameter={1},type={2}",
  5. typeof(CommonMethod).Name,TParameter.GetType().Name,TParameter);
  6. }

显示结果:
未命名图片.png
为什么泛型可以解决上面的问题呢?

泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候菜指定参数类型。延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列,EF的延迟加载等等。

泛型究竟是如何工作的呢?

控制台程序最终会编译成一个exe程序,exe被点击后,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,才会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译生成的占位符替换成具体的数据类型。请看下面的例子:

  1. Console.WriteLine(typeof(List<>));
  2. Console.WriteLine(typeof(Dictionary<,>));

结果:
未命名图片.png
从上面的截图中可以看出:泛型在编译之后会生成占位符
注意:占位符需要在英文输入法状态才能输入,只需要按一次波浪线(数字1左边的键位)即可,不需要按Shift键。
1.泛型性能问题
请看下面的一个例子,比较普通方法,Object参数类型的方法、泛型方法的性能。
添加一个Monitor类,让三种方法执行同样的操作,比较用时长短:

  1. public class Monitor
  2. {
  3. public static void Show()
  4. {
  5. Console.WriteLine("****************Monitor******************");
  6. {
  7. int iValue = 12345;
  8. long commonSecond = 0;
  9. long objectSecond = 0;
  10. long genericSecond = 0;
  11. {
  12. Stopwatch watch = new Stopwatch();
  13. watch.Start();
  14. for (int i = 0; i < 100000000; i++)
  15. {
  16. ShowInt(iValue);
  17. }
  18. watch.Stop();
  19. commonSecond = watch.ElapsedMilliseconds;
  20. }
  21. {
  22. Stopwatch watch = new Stopwatch();
  23. watch.Start();
  24. for (int i = 0; i < 100000000; i++)
  25. {
  26. ShowObject(iValue);
  27. }
  28. watch.Stop();
  29. objectSecond = watch.ElapsedMilliseconds;
  30. }
  31. {
  32. Stopwatch watch = new Stopwatch();
  33. watch.Start();
  34. for (int i = 0; i < 100000000; i++)
  35. {
  36. Show<int>(iValue);
  37. }
  38. watch.Stop();
  39. genericSecond = watch.ElapsedMilliseconds;
  40. }
  41. Console.WriteLine("commonSecond={0},objectSecond={1},genericSecond={2}"
  42. , commonSecond,
  43. objectSecond, genericSecond);
  44. }
  45. }
  46. #region PrivateMethod
  47. private static void ShowInt(int iParameter)
  48. {
  49. //do nothing
  50. }
  51. private static void ShowObject(object oParameter)
  52. {
  53. //do nothing
  54. }
  55. private static void Show<T>(T tParameter)
  56. {
  57. //do nothing
  58. }
  59. #endregion
  60. }

结果:
未命名图片.png
从结果中可以看出:泛型方法的性能最高,其次是普通方法,object方法是性能最低的。

四、泛型类

除了方法可以使泛型外,类也可以是泛型的,例如:<br />    
public class GenericClass<T> //泛型类
{
    public T _Field;
}
Main()方法中调用:
// T是int类型
GenericClass<int> genericInt = new GenericClass<int>();

genericInt._Field = 123;  
// T是string类型

GenericClass<string> genericString = new GenericClass<string>();

genericString._Field = "123";

除了可以有泛型类,也可以有泛型接口,例如:

public interface IGenericInterface<T> //泛型接口
{
    T GetT(T t);
}
泛型委托:
public delegate void GenericDelegate<T>(T t); //泛型委托
注意:<br />       1.泛型在声明的时候可以不指定具体的类型,但是在使用的时候必须指定具体类型,例如:
/// <summary>
/// 使用泛型的时候必须指定具体类型,
/// 这里的具体类型是int
/// </summary>

public class CommonClass :GenericClass<int>
{
}


2.类实现泛型接口也是这种情况,例如:

    /// <summary>
    /// 必须指定具体类型
    /// </summary>
    public class Common : IGenericInterface<string>
    {
        public string GetT(string t)
        {
            throw new NotImplementedException();
        }

    }

    /// <summary>
    /// 可以不知道具体类型,但是子类也必须是泛型的
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class CommonChild<T> : IGenericInterface<T>
    {
        public T GetT(T t)
        {
            throw new NotImplementedException();
        }
    }

五、泛型约束

先来看看下面的一个例子: 
//定义一个People类,里面有属性和方法
public interface ISports { void Pingpang(); }
public interface IWork { void Work(); }

public class People
{
    public int Id { get; set; }
    public string Name { get; set; }
    public void Hi() => Console.WriteLine("Hi");
}

public class Chinese : People, ISports, IWork
{
    public void Tradition() => Console.WriteLine("仁义礼智信,温良恭俭让!");
    public void SayHi() => Console.WriteLine("吃了么?");
    public void Pingpang() => Console.WriteLine("打乒乓球...");
    public void Work() => throw new NotImplementedException();
}

public class Hubei:Chinese
{
    public Hubei(int version) { }
    public string Changjiang { get; set; }
    public void MaJiang() => Console.WriteLine("打麻将啦...");
}

public class Japanese : ISports
{
    public int Id { get; set; }
    public string Name { get; set; }
    public void Hi() => Console.WriteLine("Hi");
    public void Pingpang() => Console.WriteLine("打乒乓球...");
}


在Main()方法里实例化

public class MainClass {
    static void Main()
    {
        People people = new People { Id = 123, Name = "走自己的路" };
        Chinese chinese = new Chinese() { Id=234,Name="晴天" };
        Hubei hubei = new Hubei(123) {Id=345,Name="流年" };
        Japanese japanese = new Japanese() 
        {
            Id=7654,Name="werwer"
        };
    }
}
    这时有一个需求:需要打印出Id和Name属性的值,将ShowObject()方法修改如下<br />![未命名图片.png](https://cdn.nlark.com/yuque/0/2020/png/1624034/1592789484866-78f94297-0890-4261-a408-583726de5a11.png#crop=0&crop=0&crop=1&crop=1&height=171&id=omsoR&margin=%5Bobject%20Object%5D&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E5%9B%BE%E7%89%87.png&originHeight=171&originWidth=755&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60161&status=done&style=none&title=&width=755)<br />        但是这样修改报错了,object类里面没有Id和Name属性,可能有人会说,强制类型转换一下就可以了:
public static void ShowObjec(object oParameter)
{

    Console.WriteLine("This
                      is {0},parameter={1},type={2}",
                      typeof(CommonMethod).Name,
                      oParameter.GetType().Name, oParameter);
    Console.WriteLine($"{((People)oParameter).Id}_{((People)oParameter).Name}");
}
   这样修改之后,代码不会报错了,这时我们在Main()方法里面调用:<br />![未命名图片.png](https://cdn.nlark.com/yuque/0/2020/png/1624034/1592789544900-48173ace-ff79-4865-8910-c1873cd1a23f.png#crop=0&crop=0&crop=1&crop=1&height=206&id=SwyXz&margin=%5Bobject%20Object%5D&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E5%9B%BE%E7%89%87.png&originHeight=206&originWidth=1323&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60161&status=done&style=none&title=&width=1323)<br />    可以看出程序报错了,因为Japanese没有继承自People,这里类型转换的时候失败了。这样会造成类型不安全的问题。那么怎么解决类型不安全的问题呢?那就是使用泛型约束。<br />        所谓的泛型约束,实际上就是约束的类型T。使T必须遵循一定的规则。比如T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单**,只需要where关键字,加上约束的条件。**<br />泛型约束总共有五种
约束 说明
T:结构 类型参数必须是值类型
T:类 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。
T:new() 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。
T:<基类名> 类型参数必须是指定的基类或派生自指定的基类。
T:<接口名称> 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。


1、基类约束
上面打印的方法约束T类型必须是People类型。
未命名图片.png
注意:
基类约束时,基类不能是密封类,即不能是sealed类。sealed类表示该类不能被继承,在这里用作约束就无任何意义,因为sealed类没有子类。
2、接口约束
未命名图片.png
3、引用类型约束class
引用类型约束保证T一定是引用类型。
未命名图片.png
4、值类型约束 struct
值类型约束保证T一定是值类型的。
未命名图片.png
5、无参数构造函数约束 new()
未命名图片.png
泛型约束也可以同时约束多个,例如:
未命名图片.png
注意:有多个泛型约束时,new()约束一定是在最后。

六、泛型的协变和逆变

协变和逆变是在.NET4.0的时候出现的,只能放在接口或者委托的泛型参数前面,out协变covariant,用修饰返回值;in逆变contravariant,用来修饰传入参数。<br />    先看下面的一个例子:<br />    定义一个Animal类:<br />![未命名图片.png](https://cdn.nlark.com/yuque/0/2020/png/1624034/1592789740073-3e9233c6-211c-4904-a013-a13f772c03bd.png#crop=0&crop=0&crop=1&crop=1&height=112&id=AnsEf&margin=%5Bobject%20Object%5D&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E5%9B%BE%E7%89%87.png&originHeight=112&originWidth=375&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60161&status=done&style=none&title=&width=375)<br />  然后在定义一个Cat类继承自Animal类:<br />![未命名图片.png](https://cdn.nlark.com/yuque/0/2020/png/1624034/1592789787972-d859600c-90e4-4422-ab65-d7a2381796d4.png#crop=0&crop=0&crop=1&crop=1&height=105&id=PlFgA&margin=%5Bobject%20Object%5D&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E5%9B%BE%E7%89%87.png&originHeight=105&originWidth=377&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60161&status=done&style=none&title=&width=377)<br />    Main()方法可以这样调用:<br />![未命名图片.png](https://cdn.nlark.com/yuque/0/2020/png/1624034/1592789797017-5834a99a-145d-4d68-92e1-ccda5430e08a.png#crop=0&crop=0&crop=1&crop=1&height=233&id=v5Pce&margin=%5Bobject%20Object%5D&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E5%9B%BE%E7%89%87.png&originHeight=233&originWidth=462&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60161&status=done&style=none&title=&width=462)<br />  那么问题来了。下面一句代码是不是正确的的?
List<Animal> list = new List<Cat>();
 可能有人会认为是正确的:因为一只Cat属于Animal,那么一群Cat也应该属于Animal啊。但是实际上这样声明是错误的:因为List<Cat>和List<Animal>之间没有父子关系。<br />![未命名图片.png](https://cdn.nlark.com/yuque/0/2020/png/1624034/1592789813347-d9fcaddc-1dfc-4f98-b1fa-6608b763a828.png#crop=0&crop=0&crop=1&crop=1&height=142&id=Yenza&margin=%5Bobject%20Object%5D&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E5%9B%BE%E7%89%87.png&originHeight=142&originWidth=1055&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60161&status=done&style=none&title=&width=1055)<br />    这时就可以用到协变和逆变了。<br />![未命名图片.png](https://cdn.nlark.com/yuque/0/2020/png/1624034/1592789825896-8f769323-8f46-4daf-bcc7-4b26daef3ded.png#crop=0&crop=0&crop=1&crop=1&height=92&id=WfZlZ&margin=%5Bobject%20Object%5D&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E5%9B%BE%E7%89%87.png&originHeight=92&originWidth=465&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60161&status=done&style=none&title=&width=465)<br />    F12查看IEnumerable定义<br />![未命名图片.png](https://cdn.nlark.com/yuque/0/2020/png/1624034/1592789838089-27b8d22d-34df-4de6-8002-610add828bd3.png#crop=0&crop=0&crop=1&crop=1&height=272&id=cKo54&margin=%5Bobject%20Object%5D&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E5%9B%BE%E7%89%87.png&originHeight=272&originWidth=803&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60161&status=done&style=none&title=&width=803)<br />    可以看到,在泛型接口的T前面有一个out关键字修饰,而且T只能是返回值类型,不能作为参数类型,这就是协变。使用了协变以后,左边声明的是基类,右边可以声明基类或者基类的子类。<br />    协变除了可以用在接口上面,也可以用在委托上面:
Func<Animal> func = new Func<Cat>(() => null);

除了使用.NET框架定义好的以为,我们还可以自定义协变,例如:
未命名图片.png
使用自定义的协变:
未命名图片.png

在来看看逆变。
在泛型接口的T前面有一个In关键字修饰,而且T只能方法参数,不能作为返回值类型,这就是逆变。请看下面的自定义逆变:
未命名图片.png
使用自定义逆变:
未命名图片.png
协变和逆变也可以同时使用,看看下面的例子:
未命名图片.png
使用:
未命名图片.png

七、泛型缓存

在前面我们学习过,类中的静态类型无论实例化多少次,在内存中只会有一个。静态构造函数只会执行一次。在泛型类中,T类型不同,每个不同的T类型,都会产生一个不同的副本,所以会产生不同的静态属性、不同的静态构造函数,请看下面的例子:
未命名图片.png
然后新建一个测试类,用来测试GenericCache类的执行顺序:
未命名图片.png
Main()方法里面调用:
未命名图片.png
从上面的截图中可以看出,泛型会为不同的类型都创建一个副本,所以静态构造函数会执行5次。 而且每次静态属性的值都是一样的。利用泛型的这一特性,可以实现缓存。
注意:只能为不同的类型缓存一次。泛型缓存比字典缓存效率高。泛型缓存不能主动释放