泛型
什么是泛型
泛型是在.Net Framework 2.0时推出的语法(非语法糖);在各项目中是最为常见,可见其重要性。
泛型主要是基于解决“复用”的问题上衍生而出,下面且听我娓娓道来……
为什么使用泛型
在1.0版本时,是通过OOP三大特性之一的继承,变相生成“泛型”的雏形。
代码及执行结果如下:
1 | public static void ShowObjeck(object oParameter) |
从结果中不难看出,实现了代码的可复用性;其实是根据以下两条理论实现的:
- object类型是一切类型的父类。
- 通过继承,子类拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替。
但是,objeck类型的方法又引出另外一个问题:装箱、拆箱;损耗程序的性能。
好在微软在.net framework2.0时,推出了泛型!(此刻,应有欢呼~ヾ(@^▽^@)ノ)
1. 泛型的好处和原理
先从一个简单的实例,了解一下泛型;与通过objeck实现的“泛型”存在明显差异,关键在于泛型类的参数。
1 | /// <summary> |
- 在泛型类型或方法定义中,类型参数是在实例化泛型类型的一个变量时,客户端指定的特定类型的占位符。
- 泛型参数(
<T>
)无法按原样使用,它不是真正的数据类型,更像是数据类型的模板。 若要使用<T>
,客户端代码必须通过指定尖括号内的类型参数来声明并实例化构造类型。- 此特定类的类型参数可以是编译器可识别的任何类型。 可创建任意数量的构造类型实例,其中每个使用不同的类型参数。
- 泛型参数的声明切勿使用关键字;尽量不要使用项目中已命名的名称。
通过上述内容,我们需要明确一下,什么情况下使用泛型方法;
即,是用一个方法,满足不同参数类型,实现相同的逻辑。
调用泛型方法,展示的效果如下:
普通方法在声明的时候,就已经确定参数类型,约定俗成,在调用该方法时,也只能使用定义好的参数类型。通过objeck变相实现的“泛型”,虽然什么类型都可以传入,但也带来了拆、装箱的问题。
泛型方法只有调用的时候才确定数据类型;白话讲,没有写死参数类型;这也是泛型中最突出的特性。
2. 引入泛型:延迟声明
在调用时才制定数据类型;在涉及思想上,叫延迟声明。泛型就是把参数类型的声明延迟到调用;提高代码的可塑性。
设计系统过程中,推崇一句话:推迟一切可以推迟的。即,延迟思想。
题外话,当前移动端里健全的APP程序,在展示的时候都会采用延迟思想;比如,产品详情中,10张图,先展示3张,其余资源是在用户查看过程中加载完毕。这样既提高用户体验感,又减少己方服务器的压力。深层次讲,该思路几乎无处不在,像EF的延迟加载等等
3. 如何声明和使用泛型
泛型不是语法糖;而是.net framework2.0由框架升级提供的功能。需要编译器和JIT支持。
控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译生成的占位符替换成具体的数据类型。
通过下面的实例验证一下:
从实例中能够看出:泛型在编译之后会生成占位符。
注意:占位符需要在英文输入法状态下才能输入,只需要按一次波浪线(数字1左边的键位)的键位即可,不需要按Shift键。
创建一个Monitor类,分别对三种方法(普通方法、Object参数类型的方法、泛型方法 )进行比较用时。
1 | using System; |
从调用结果中能够看出,性能优越的依次是泛型方法、普通方法和objeck变相方法。
4. 泛型类、泛型方法、泛型接口和泛型委托
既然有泛型方法,泛型类、泛型接口和泛型委托就应然而生了。
1 | /// <summary> |
1 | /// <summary> |
1 | /// <summary> |
- 普通类可以继承泛型类和泛型接口;既然是继承,就是在使用泛型类(或者泛型接口),所以要指定泛型类型。
1 | /// <summary> |
1 | /// <summary> |
- 子类也是泛型,继承时可以不指定具体类型。
1 | /// <summary> |
5. 泛型约束
泛型约束,实际上就是约束的类型
。使 必须遵循一定的规则。比如T必须继承自某个类,或者 必须实现某个接口等等。
1 | using System; |
建立上面的People类后,再通过调用CommonMethod类中的ShowObjeck方法,实现每个对象输出Id和Name的值。这里,需要对ShowObjeck方法进行改造,并在main()调用。
1 | public static void ShowObjeck(object oParameter) |
1 | People people = new People() |
在执行调用时,出现了异常。因为Japanese没有继承自People,这里类型转换的时候失败 ;除了本身装箱、拆箱的性能损耗,这个暂且不提,这里主要是演示造成类型的不安全。为了规范(约束)类型,由此便需要泛型约束。
泛型最主要的特性就是能接纳不同类型的参数。但也促成对类型识别存在“隐患”,也就是在泛型方法中不知道接纳类型的具体内容,也就无法操作接纳的类型。
泛型为了解决该问题,通过关键字where,加上约束条件,也就形成了泛型约束。
泛型约束共计5种:
约束名称 | 约束条件 | 约束说明 |
---|---|---|
基类约束 | T:<基类名> | 类型参数必须是指定的基类或派生自指定的基类。 |
接口约束 | T:<接口名称> | 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。 |
引用类型约束 | T:类 | 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。 |
值类型约束 | T:结构 | 类型参数必须是值类型 |
无参数构造函数约束 | T:new() | 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。 |
1 | public static void Show<T, A, B>(T t, A a, B b) |
Notes:
- 可以使用基类的属性、方法。
- 基类约束时,基类不能是密封类,即不能是sealed类。sealed类表示该类不能被继承,用作约束就无任何意义,因为sealed类没有子类。
- 这里延伸一下,可以对多个值进行约束,但不提倡,了解一下即可。
1 | /// <summary> |
1 | /// <summary> |
1 | /// <summary> |
1 | /// <summary> |
泛型约束也可以同时约束多个;注意:有多个泛型约束时,new()约束一定是在最后。 例如:
1 | public static void Show<T>(T tParameter) |
6. 协变和逆变
这两个语法是在.Net 4.0出现的。是为了解决:基类泛型集合无法生成子对象集合。
协变和逆变只能用到接口或者委托的泛型参数前面;out 协变covariant,用来修饰返回值;in:逆变contravariant,用来修饰传入参数。
下面通过实例了解一下;分别建立两个类,并呈继承关系,并调用。
1 | public class Animal |
可能有人会认为是正确的:因为一只Cat属于Animal,那么一群Cat也应该属于Animal啊。但是实际上这样声明是错误的:因为List
和List 之间没有父子关系;他们是两个集合。
协变就可以解决该问题:
IEnumerable<Animal> List2 = new List<Cat>();
通过IEnumerable定义可以看出,泛型类型T前面有一个out关键字修饰。而且T只能是返回值类型,不能作为参数类型,这就是协变。使用了协变以后,左边声明的是基类,右边可以声明基类或者基类的子类。
现在说说逆变,是在泛型接口的T前面有一个In关键字修饰,而且T只能方法参数,不能作为返回值类型,这就是逆变。 大家有兴趣可以查一下相关资料,这里不做赘述了;因为在工作中,几乎没见过逆变,即便是架构搭建中。
7. 泛型缓存
复习一下静态基础内容,因为泛型缓存会颠覆部分概念。
- 静态构造函数只会执行一次;很好的诠释了DRY(Don’t Repeat Yourself)原则。
- 静态成员(内容) 在内存中唯一的,只存储一份;有个别“极端主义者”直接用来保存用户信息……
因为泛型的特性,接纳不同的类型,相应就会产生一份不同的副本(静态内容)。这是一个贼酷发现
~适合用于不同类型,需要缓存一份数据的场景,效率高。
通过下面的实例来了解一下。
1 | using System; |
1 | using System; |
从上面的截图中可以看出,泛型会为不同的类型都创建一个副本,所以静态构造函数会执行5次。 而且每次静态属性的值都是一样的。利用泛型的这一特性,可以实现缓存。
只能为不同的类型缓存一次。泛型缓存比字典缓存效率高。泛型缓存不能主动释放。
发布时间: 2020-01-28
最后更新: 2020-02-09
本文标题: 泛型
本文链接: http://www.selectcode.cn/2020/01/28/Generic/
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可。转载请注明出处!