第五节:泛型(泛型类、接口、方法、委托、泛型约束、泛型缓存、逆变和协变)
一. 泛型诞生的背景
在介绍背景之前,先来看一个案例,要求:分别输出实体model1、model2、model3的id和name值,这三个实体有相同的属性名字id和name。
1 public class myUtils 2 { 3 //要求:分别输出实体model1、model2、model3的id和name值,这三个实体有相同的属性名字id和name 4 5 //传统的解决方案(一):由于三个不同的实体,所以要声明三个不同的方法来输出 6 /* 7 缺点:该解决方案十分繁琐,明明相同的属性,不得不声明三个不同的方法,造成代码的大量冗余 8 */ 9 10 public static void showM1(model1 m1) 11 { 12 Console.WriteLine("id值为:" + m1.id + " name值为:" + m1.name); 13 } 14 15 public static void showM2(model2 m2) 16 { 17 Console.WriteLine("id值为:" + m2.id + " name值为:" + m2.name); 18 } 19 20 public static void showM3(model3 m3) 21 { 22 Console.WriteLine("id值为:" + m3.id + " name值为:" + m3.name); 23 } 24 25 26 //传统的解决方案(二):使用Object类型来解决该问题 27 //原理:object是所有类型的父类,所有用到父类的地方都可以用子类去代替,即:里氏替换原则 28 /* 29 缺点:需要记住可能会传进来哪几种类型;最大的问题是值类型和引用类型之间的互换(即拆箱和装箱)严重影响性能 30 */ 31 32 public static void showObj(object obj) 33 { 34 if (obj.GetType() == typeof(model1)) 35 { 36 model1 m = (model1)obj; 37 Console.WriteLine("id值为:" + m.id + " name值为:" + m.name); 38 } 39 else if (obj.GetType() == typeof(model2)) 40 { 41 model2 m = (model2)obj; 42 Console.WriteLine("id值为:" + m.id + " name值为:" + m.name); 43 } 44 else if (obj.GetType() == typeof(model3)) 45 { 46 model3 m = (model3)obj; 47 Console.WriteLine("id值为:" + m.id + " name值为:" + m.name); 48 } 49 50 } 51 52 //基于以上两种传统的解决方案都缺点明显,所以在 .Net 2.0的时候,推出了一个通用语言运行时(CRL)的新特性即:泛型。 53 /* 54 泛型为.NET框架引入了类型参数(type parameters)的概念。类型参数使得设计类和方法时,不必确定一个或多个具体参数,其的具体参数可延迟到客户代码中声明、实现。 55 这意味着使用泛型的类型参数T,可以多种形式调用,运行时类型转换避免了装箱操作的代价和风险。 56 57 */ 58 59 }
基于以上两种传统的解决方案都缺点明显,所以在 .Net 2.0的时候,推出了一个通用语言运行时(CLR)的新特性即:泛型。泛型为.NET框架引入了类型参数(type parameters)的概念,它属于System.Collections.Generic命名空间下。类型参数使得设计类和方法时,不必确定一个或多个具体参数,其的具体参数可延迟到客户代码中声明、实现。
这意味着使用泛型的类型参数T,可以多种形式调用,运行时类型转换避免了装箱操作的代价和风险。
二. 泛型的引入
为了解决上述问题,我们这里引入泛型的概念来解决代码冗余的问题和性能低下的问题。
这里统一说明一下:model1、model2、model3实体均继承与ModelFather实体,ModelFather实体中有两个属性,id和name,下同。
1 /// <summary> 2 /// 泛型方法的引入 3 /// </summary> 4 /// <typeparam name="T">T为泛型的一个标记,也可以用别的字母代替</typeparam> 5 /// <param name="t"></param> 6 public static void showModel<T>(T t) 7 { 8 Console.WriteLine(t); 9 } 10 11 /// <summary> 12 /// 泛型约束的引入,如果要输出实体中的值,需要配合“基类约束”方可以实现 13 /// </summary> 14 /// <typeparam name="T"></typeparam> 15 /// <param name="t"></param> 16 public static void showModelDetils<T>(T t) where T : ModelFather 17 { 18 Console.WriteLine("id值为:" + t.id + " name值为:" + t.name); 19 } 20 21 22 //总结: 23 /* 24 1. 以上两个方法均为泛型方法,T代表的类型在使用时才声明,俗称“延迟声明”。 25 2. 如果要使用类中的属性时,需要配合泛型的“基类约束”来实现 26 */
总结:
1. 以上两个方法均为泛型方法,T代表的类型在使用时才声明,俗称“延迟声明”。
2. 如果要使用类中的属性时,需要配合泛型的“基类约束”来实现。
说明:除了泛型方法外,还有泛型接口、泛型委托等,同样泛型约束除了“基类约束”外,还有其他约束。
三. 泛型的种类
泛型包括:泛型方法、泛型类、泛型接口、泛型委托四类。
这里主要介绍泛型方法,在后续会配合泛型的五种约束继续深入介绍。

1 /// <summary> 2 /// 泛型方法 3 /// </summary> 4 public class GenericsMethord 5 { 6 //这里介绍泛型方法,在之前02-泛型的引入中,使用的就是泛型方法,这里再重复一次 7 /* 8 详解:T为泛型的一个代表,换成别的字母同样可以 9 T和A代表的类型在使用时才声明,俗称“延迟声明” 10 */ 11 public static void ShowModel<T, A>(T model1, A model2) 12 where T : ModelFather 13 where A : model3 14 { 15 Console.WriteLine("id值为:" + model1.id + " name值为:" + model1.name); 16 Console.WriteLine("id值为:" + model2.id + " name值为:" + model2.name); 17 } 18 }

1 /// <summary> 2 /// 泛型类 3 /// </summary> 4 public class GenericsClass<T> 5 { 6 public string id { get; set; } 7 public string name { get; set; } 8 9 public void Test(T t) 10 { 11 Console.WriteLine(t.GetType()); 12 } 13 }

1 /// <summary> 2 /// 泛型接口 3 /// </summary> 4 public class GenericsInterface 5 { 6 public interface IGet<T> 7 { } 8 }

1 /// <summary> 2 /// 泛型委托 3 /// </summary> 4 public class GenericsDelegate 5 { 6 public delegate void GetHandler<T>(); 7 8 }
四.泛型约束
(一). 泛型约束主要分为以下五种:
1. 基类约束:where T:<基类名>
2. 接口约束:where T:<接口名称> 约束的接口也可以是泛型的
3. 无参构造函数约束:where T: new() 提供的任何类型参数都必须具有可访问的无参数(或默认)构造函数。当与其他约束一起使用时,new() 约束必须最后指定。
4. 值类型约束:where T:struct 类型参数必须是值类型,可以指定除 Nullable 以外的任何值类型。
5. 引用类型约束: where T:class 类型参数必须是引用类型,也适用于任何类、接口、委托或数组类型。当与其他约束一起使用时,class 约束必须最先指定。
(二). 多参数约束:
可以写多个where条件
public static void ShowModel<T, A>(T model1, A model2)
where T : ModelFather
where A : model3
(三):多条件约束:
当有多个条件约束时,无参构造函数约束要写到最后
public void ShowManyCon<T>(T t) where T : class, IWork, ISport, new()
(四):分别测试各种约束
A:各种约束的代码
1 public class genericsConstraint 2 { 3 4 /// <summary> 5 /// 1. 基类约束和接口约束 6 /// </summary> 7 /// <typeparam name="T"></typeparam> 8 /// <param name="t"></param> 9 public void Show<T>(T t) where T : People, IWork 10 { 11 Console.WriteLine("id值为:" + t.id + " name值为:" + t.name); 12 t.Say(); 13 t.Work(); 14 } 15 /// <summary> 16 /// 2. 无参构造函数约束 17 /// </summary> 18 /// <typeparam name="T"></typeparam> 19 /// <param name="t"></param> 20 public void ShowNo<T>(T t) where T : new() 21 { 22 T t1 = new T(); 23 Console.WriteLine(t1); 24 25 } 26 /// <summary> 27 /// 3. 值类型约束 28 /// C#值类型包括:结构体、数据类型(整型、字符型、浮点型、decimal型)、bool型、枚举、可空类型 29 /// </summary> 30 /// <typeparam name="T"></typeparam> 31 /// <param name="t"></param> 32 public void ShowZhi<T>(T t) where T : struct 33 { 34 Console.WriteLine(t); 35 } 36 /// <summary> 37 /// 4. 引用类型约束 38 /// C#引用类型包括:数组、类、接口、委托、object、字符串 39 /// </summary> 40 /// <typeparam name="T"></typeparam> 41 /// <param name="t"></param> 42 public void ShowYin<T>(T t) where T : class 43 { 44 Console.WriteLine(t); 45 } 46 /// <summary> 47 /// 5. 多参数约束 48 /// </summary> 49 /// <typeparam name="T"></typeparam> 50 /// <typeparam name="G"></typeparam> 51 /// <param name="t"></param> 52 /// <param name="g"></param> 53 public void ShowMany<T, G>(T t, G g) 54 where T : People 55 where G : IWork 56 { 57 Console.WriteLine(t); 58 Console.WriteLine(g); 59 } 60 /// <summary> 61 /// 6. 多条件约束 62 /// </summary> 63 /// <typeparam name="T"></typeparam> 64 /// <param name="t"></param> 65 public void ShowManyCon<T>(T t) where T : class, IWork, ISport, new() 66 { 67 68 }
B:各种实体代码
1 namespace Generics._04_泛型约束 2 { 3 /// <summary> 4 /// People类 5 /// </summary> 6 public class People 7 { 8 public string id { get; set; } 9 10 public string name { get; set; } 11 12 public void Say() 13 { 14 Console.WriteLine("我会说话"); 15 } 16 17 } 18 /// <summary> 19 /// Chinese类 20 /// </summary> 21 public class Chinese : People, ISport, IWork 22 { 23 public void Sport() 24 { 25 Console.WriteLine("我在运动"); 26 } 27 28 public void Work() 29 { 30 Console.WriteLine("我在工作"); 31 } 32 } 33 /// <summary> 34 /// Janpanese类 35 /// </summary> 36 public class Janpanese : IWork 37 { 38 public string id { get; set; } 39 40 public string name { get; set; } 41 public void Work() 42 { 43 Console.WriteLine("我在工作"); 44 } 45 } 46 /// <summary> 47 /// YanTai 48 /// </summary> 49 public class YanTai : Chinese 50 { 51 52 } 53 /// <summary> 54 /// IWork接口 55 /// </summary> 56 public interface IWork 57 { 58 void Work(); 59 } 60 /// <summary> 61 /// ISport接口 62 /// </summary> 63 public interface ISport 64 { 65 void Sport(); 66 } 67 /// <summary> 68 /// 构造函数无参的 类 69 /// </summary> 70 public class Test1 71 { 72 public Test1() 73 { 74 75 } 76 } 77 /// <summary> 78 /// 构造函数有参的类 79 /// </summary> 80 public class Test2 81 { 82 public Test2(string id,string name) 83 { 84 85 } 86 } 87 88 89 }
C:初始化代码
1 //1.四个实体类 2 People people = new People() 3 { 4 id = "p1", 5 name = "mr1" 6 }; 7 Chinese chinese = new Chinese() 8 { 9 id = "p2", 10 name = "mr2" 11 }; 12 Janpanese janpanese = new Janpanese() 13 { 14 }; 15 YanTai yanTai = new YanTai() 16 { 17 id = "p3", 18 name = "mr3" 19 }; 20 //2. 构造函数有参数和无参数的两个类 21 Test1 test1 = new Test1(); 22 Test2 test2 = new Test2("pp","maru"); 23 //3. 声明一个值类型和一个引用类型 24 int num = 12; //值类型 25 YanTai yt2 = new YanTai(); //引用类型
D:代码调用
1 Console.WriteLine("------------------------------------四 泛型约束------------------------------------"); 2 //4.1 基类约束和接口约束 3 Console.WriteLine("------------------------------------4.1 基类约束和接口约束------------------------------------"); 4 genericsConstraint gc = new genericsConstraint(); 5 //gc.Show<People>(people); //People类没有实现IWork接口 6 gc.Show<Chinese>(chinese); 7 //gc.Show<Janpanese>(janpanese); //Janpanese只实现了IWork接口,但不满足基类约束(即使他有和基类相同的属性也不行) 8 gc.Show<YanTai>(yanTai); 9 //4.2 无参构造函数约束 10 Console.WriteLine("------------------------------------4.2 无参构造函数约束------------------------------------"); 11 gc.ShowNo<Test1>(test1); 12 // gc.ShowNo<Test2>(test2); //Test2的构造函数有参数,所以不满足无参构造函数约束 13 //4.3 值类型约束 14 Console.WriteLine("------------------------------------4.3 值类型约束------------------------------------"); 15 gc.ShowZhi<int>(num); 16 //gc.ShowZhi<YanTai>(yt2); //YanTai类 是引用类型,不满足值类型约束 17 //4.4 引用类型约束 18 Console.WriteLine("------------------------------------4.4 引用类型约束------------------------------------"); 19 //gc.ShowYin<int>(num); //int类型 是值类型,不满足引用类型约束 20 gc.ShowYin<YanTai>(yt2); 21 //4.5 多参数约束 22 Console.WriteLine("------------------------------------4.5 多参数约束------------------------------------"); 23 gc.ShowMany<People, Chinese>(people, chinese); 24 //4.6 多条件约束 25 Console.WriteLine("------------------------------------4.6 多条件约束------------------------------------"); 26 Console.WriteLine("针对多条件约束,这里不做测试");
D:测试结果
五. 泛型的原理和好处
(一). 泛型的原理:
延时声明,在运行时进行编译
(二). 泛型的好处:
1.减少代码冗余量,精简代码
2.避免了拆箱和装箱过程过程中代理的性能损失
3.结合IDE的只能提示,提高了开发效率
下面测试一下:普通方法、使用Object类型、使用泛型 三种情况对同一个数据进行处理,耗时情况
1 public class utils 2 { 3 /// <summary> 4 /// 正常方法 5 /// </summary> 6 /// <param name="iParameter"></param> 7 public static void ShowCommon(int iParameter) 8 { } 9 /// <summary> 10 /// object方法 11 /// </summary> 12 /// <param name="oParameter"></param> 13 public static void ShowObject(object oParameter) 14 { } 15 /// <summary> 16 /// 原型方法 17 /// </summary> 18 /// <typeparam name="T"></typeparam> 19 /// <param name="tParameter"></param> 20 public static void ShowGeneric<T>(T tParameter) 21 { } 22 }
1 Console.WriteLine("----------测试正常方法、Object方法、泛型方法各自耗时情况------------"); 2 long commonTime = 0; 3 long objectTime = 0; 4 long genericTime = 0; 5 { 6 Stopwatch stopwatch = new Stopwatch(); 7 stopwatch.Start(); 8 9 for (int i = 0; i < 1000000000; i++) 10 { 11 utils.ShowCommon(ivalue); 12 } 13 stopwatch.Stop(); 14 commonTime = stopwatch.ElapsedMilliseconds; 15 } 16 { 17 Stopwatch stopwatch = new Stopwatch(); 18 stopwatch.Start(); 19 20 for (int i = 0; i < 1000000000; i++) 21 { 22 utils.ShowObject(ivalue); 23 } 24 stopwatch.Stop(); 25 objectTime = stopwatch.ElapsedMilliseconds; 26 } 27 { 28 Stopwatch stopwatch = new Stopwatch(); 29 stopwatch.Start(); 30 31 for (int i = 0; i < 1000000000; i++) 32 { 33 utils.ShowGeneric<int>(ivalue); 34 } 35 stopwatch.Stop(); 36 genericTime = stopwatch.ElapsedMilliseconds; 37 } 38 Console.WriteLine("commonTime = {0} objectTime = {1} genericTime = {2}", commonTime, objectTime, genericTime);
测试结果:
结论:正常的方法和泛型方法耗时基本一致,泛型方法甚至用的时间更短,但是采用object类型转换的方法,耗时明显长于其他两种
六. 补充default(T)
了解:
1. 在泛型中如果需要返回泛型类型的默认值则会用到这个关键字。
2. T是值类型而非结构的则default(T) 数值类型返回0,字符串返回空。
3. 是非引用类型是结构时候返回初始化为零或空的每个结构成员。
4. 引用类型返回NULL
5. 其实就是为了返回默认值,比如int i =0;这样是可以的,但是int i=null是不可以的,但是泛型的时候不知道是值类型还是引用类型所以不知道如何赋默认值。
用这个关键字就解决了这个问题
七. 泛型缓存
1. 知识普及:如果一个(普通)类中有静态变量或者静态构造函数,当实例化的时候,CLR会优先初始化静态变量和静态构造函数,且只有在第一次实例化的时候进行初始化,后续都不在进行初始化。(PS:利用这两个特性可以创建单例模式)
特别注意:如果是泛型类的话,不同类型的第一次实例化的时候都要初始化静态变量和静态构造函数(每个不同的T,都会生成一份不同的副本)。
2. 先用一普通例子来抛砖引玉:
新建一个CommonClass类,里面有静态字段和静态方法,然后调用4次,发现每次输出的时间都是相同,证明静态字段和静态构造函数只有第一次实例化的时候才调用。
或者通过加断点的形式,也可以发现,只有在第一次实例化的时候才能进入静态字段和静态构造函数,同样证明了上述结论。
3. 下面建一个泛型类GenericsClass<T>,里面有静态字段和静态方法,然后分别传入不同的类型,int、string、int、string,发现输出的时间第2次比第1次时间晚约2s,但是第1次和第3次,第2次和第4次的时间是相同,这就是泛型缓存。
4. 最后自己总结一下泛型缓存的定义:
对于泛型类而言,不同类型都会生成不同的副本(即都要调用静态的构造函数和静态字段),但相同的类型,实例化一次后,再次实例化,将不会在生成副本(即不再调用静态类和静态构造函数),这就是泛型缓存。
代码分享:
1 /// <summary> 2 /// 一个普通类 3 /// </summary> 4 public class CommonClass 5 { 6 static CommonClass() 7 { 8 _InitTime = string.Format("调用构造函数的时间:{0}", DateTime.Now.ToString("yyyyMMddHHmmss.fff")); 9 } 10 private static string _InitTime = ""; 11 public void show() 12 { 13 Console.WriteLine(_InitTime); 14 } 15 } 16 /// <summary> 17 /// 泛型类 18 /// </summary> 19 /// <typeparam name="T"></typeparam> 20 public class GenericsClass<T> 21 { 22 static GenericsClass() 23 { 24 _InitTime = string.Format("调用构造函数的时间:{0}", DateTime.Now.ToString("yyyyMMddHHmmss.fff")); 25 } 26 private static string _InitTime = ""; 27 public void show() 28 { 29 Console.WriteLine(_InitTime); 30 } 31 }
1 public static void ShowGenericsCacheDemo() 2 { 3 4 { 5 Console.WriteLine("1. 测试普通类"); 6 CommonClass c1 = new CommonClass(); 7 c1.show(); 8 Thread.Sleep(2000); 9 CommonClass c2 = new CommonClass(); 10 c2.show(); 11 Thread.Sleep(2000); 12 CommonClass c3 = new CommonClass(); 13 c3.show(); 14 } 15 { 16 Console.WriteLine("2. 测试泛型类"); 17 GenericsClass<int> c1 = new GenericsClass<int>(); 18 c1.show(); 19 Thread.Sleep(2000); 20 GenericsClass<string> c2 = new GenericsClass<string>(); 21 c2.show(); 22 Thread.Sleep(2000); 23 GenericsClass<int> c3 = new GenericsClass<int>(); 24 c3.show(); 25 Thread.Sleep(2000); 26 GenericsClass<string> c4 = new GenericsClass<string>(); 27 c4.show(); 28 }
结果:
八. 逆变和协变
1. 目标:了解是什么即可,实际中用的很少。
2. 事前准备:BaseClass和DerivedClass两个类,DerivedClass类继承BaseClass类。

1 public class BaseClass 2 { 3 public string id { get; set; } 4 5 public int age { get; set; } 6 7 public string name { get; set; } 8 } 9 public class DerivedClass : BaseClass 10 { 11 12 }
3. 逆变:借助Action<in T>,将父类参数的委托赋值给子类参数的委托
4. 协变:借助IEnumerable<out T>,可以将原本有继承关系的两个类的集合关联起来
分享相关代码:
1 { 2 //里氏替换原则(也是多态的一种) 3 BaseClass bClass = new DerivedClass(); 4 } 5 { 6 //推断:List集合进行继承(编译不过,报错) 7 //List<BaseClass> bClassList = new List<DerivedClass>(); 8 } 9 { 10 //借助Action<in T>,将父类参数的委托赋值给子类参数的委托 11 //这就是 → “逆变” 12 Action<BaseClass> bAction = (a) => { Console.WriteLine(a.GetType().Name); }; 13 Action<DerivedClass> dAction = bAction; 14 dAction(new DerivedClass()); 15 } 16 { 17 //借助IEnumerable<out T>,可以将原本有继承关系的两个类的集合关联起来 18 //这就是 → “协变” 19 IEnumerable<BaseClass> bClassList = new List<DerivedClass>(); 20 }
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步