【C#进阶系列】12 泛型
泛型是CLR和编程语言提供的一种特殊机制,它用于满足“算法重用” 。
可以想象一下一个只有操作的参数的数据类型不同的策略模式,完全可以用泛型来化为一个函数。
以下是它的优势:
- 类型安全
- 给泛型算法应用一个具体的数据类型时,如果不兼容这种类型,就会编译错误或者报异常。
- 更清晰的代码
- 减少了强制转换,让代码更简洁
- 更佳的性能
- 用泛型可以有效避免装箱拆箱的操作,且无需在进行强制转换时验证是否类型安全,这两点都有效提高了代码的性能。
这就是为什么List<T>淘汰了ArrayList的原因,特别是在进行值类型操作时,因为装箱拆箱过多而差距很大。
约定:泛型参数要么为T要么以大写T开头,例如List<T>。
FCL中的泛型
System.Collections.Generic和System.Collections.ObjectModel命名空间中提供了多个泛型集合类和接口。
System.Collections.Concurrent命名空间则提供线程安全的泛型集合类。
System.Array类则提供了大量的静态泛型方法。
泛型的基础结构
.net 2.0才有泛型。
- 开放类型和封闭类型
- 之前我们讲到CLR会为各种类型创建类型对象,同样一个新的泛型类TroyList<T>也会创建一个类型对象,我们将具有泛型参数的类型称为开放类型。
- 不能构造开放类型的实例
- 而指定了泛型类型实参的泛型类型称为封闭类型,例如:TroyList<int>。
- 可以构造封闭类型的实例
- 如果TroyList<T>定义了静态字段或者方法,那么TroyList<int>和TroyList<string>之间并不共享,因为这其实是两个不同的类型对象。
- 之前我们讲到CLR会为各种类型创建类型对象,同样一个新的泛型类TroyList<T>也会创建一个类型对象,我们将具有泛型参数的类型称为开放类型。
- 泛型类型的继承
- 使用泛型类型并指定类型实参后,实际上是一个新的封闭类型,新的类型对象从泛型类型派生自的那个类型派生。即List<T>派生自Object,那么List<int>就派生自Object。
- 关于代码爆炸的优化
- 看到这里你可能想到了,一个开放类型实际上会有多个封闭类型,比如一个List<T>会有List<int>,List<string>等N多封闭类型。实际上就是N多的类型对象,生成N多的重复代码,于是这被称作代码爆炸。
- 关于优化:
- 两个不同的程序集用到同一种封闭类型,只会由JIT编译器变异一次
- CLR认为所有用引用类型做类型实参的封闭类型完全相同,所以代码可以共享。也就是说List<String>和List<Stream>的方法编译后的代码可以通用。因为操作的不同的引用类型的地址大小都是一样的。
委托和接口的逆变和协变泛型类型实参
泛型委托和接口的每个泛型类型参数都可标记为协变量和逆变量,利用此功能可实现相同类型但实参类型不同的委托和接口的相互转换。(很绕,不明白可以看下面)
- 不变量
- 意味着泛型类型参数不可更改
- 逆变量
- 意味着泛型类型参数可以从一个类更改为它的派生类。用in标记,逆变量泛型类型参数只能出现在输入位置。
- 协变量
- 意味着泛型类型参数可以从一个类更改为它的基类。用out标记,协变量泛型类型参数只能出现在输出位置。
举个例子
public class 基类 { }
public class 派生类 : 基类 { }
public class Test{
public delegate TResult MyFunc<in T1, out TResult, T2>(T1 a, T2 b);//第一个为逆变量,第二个为协变量,第三个为不变量
void show() {
MyFunc<基类, 基类, 基类> fn1 = null;
//以下注释为我自己的理解方式,只是为了方便理解而已
MyFunc<派生类, 基类, 基类> fn2 = fn1;//MyFunc<派生类, 派生类, 基类> fn2 = fn1;转换错误
MyFunc<基类, Object, 基类> fn3 = fn1;//MyFunc<Object, Object, 基类> fn3 = fn1;转换错误
MyFunc<派生类, Object, 基类> fn4 = fn1;
}
}
依然很绕,实际上不懂也没关系,转换不了编译器自然会提示。了解有这个东西就行了,也建议用int和out指定泛型委托的类型变量。更多的时候我们会用自带的泛型委托Action和Func,这两个泛型委托的参数都用到in和out。
关于泛型方法的类型推断
void Go() {
String s1 = "";
Object s2 = "";
Show(s1, s2);//不指定Show<T>的T的玩法就叫类型推断,类型推断通过传入的变量s1和变量s2的变量类型来推断,而不是实际类型。因为这里两个变量类型不同,所以函数编译不通过。
}
void Show<T>(T a,T b) {
}
约束
泛型的约束是一个很有意思的事情。
void Show<T>(T a,T b) where T :IList { }
比如上面这个函数,约束传入的类型T必须实现了IList接口。
通过约束可以限制传入的类型,然而正式因为提供了这层约束,保证了传入的类型都实现了IList接口,我们就可以使用IList的各种方法了。
约束分类:
- 主要约束
- 主要约束可以是代表非密封类的一个引用类型。(可以指定0到1个主要约束)
- 两个特殊的主要约束为class和struct,分别约束传入的参数为引用类型和值类型。(特例的特例,struct不能约束Nullable<T>)
- 约束不能指定以下特殊引用类型:Object,Array,Delegate,MulticastDelegate,ValueType,Enum或者Void。
- 次要约束
- 次要约束代表接口类型。(可以指定0到多个次要约束)
- 特殊的次要约束,即指定的两个泛型类型参数中,一个继承另一个,例如:where T2:T1。
- 构造器约束
- 构造器约束约束类型实参,一定是实现了公共无参构造函数的非抽象类型。(可以指定0到1个构造器约束)
- 所有值类型都隐式提供了公共无参构造器。所以同时使用struct和new()约束被认为是多余的,会报错。
可验证性
以下几种情况因为代码不可验证是否合法,所以将报错:
- 泛型类型变量的转换
- 原因:不可将泛型类型T的变量转换为其它类型,因为T可能为任何变量,所以可能转换失败
void Show<T>(T obj){
string a=(string)obj; //出错
}- 解决方案:
void Show<T>(T obj)
{
string a = obj as string;//对于string而言,其实这里用ToString方法可能更恰当一点
}值类型可以先强制转换为object,再转为具体的值类型。然而我认为这样的代码还是需要开箱装箱的,也许可以考虑修改下算法。
- 将泛型类型变量设为默认值
- 原因:因为T可以是值类型和引用类型,所以不可能设置一个值类型或者引用类型的默认值
- 解决方案:可以考虑加约束或者用default(T),作为默认值。
- 两个泛型类型变量相互比较
- 原因:因为非基元类型的值类型除非重载了==操作符,否则会报错。
- 解决方案:可以考虑约束为class或者用Equals。(注意哦,有可能因为Equals的被覆盖所以具体不确定是判断同一性还是相等性)
- 泛型类型变量作为操作数使用
- 原因:因为非基元类型的值类型除非重载了操作符,否则会报错。
- 解决方案:反射,操作符重载或者dynamic。(会有性能影响,我一般用dynamic了)
【C#进阶系列】12 泛型的更多相关文章
- .Net进阶系列(12)-异步多线程(Thread和ThreadPool)(被替换)
一. Thread多线程 1. 两种使用方式 通过F12查看Thread后,发现有两类构造函数,ParameterizedThreadStart和ThreadStart,其中 ThreadStar ...
- Ext JS学习第十六天 事件机制event(一) DotNet进阶系列(持续更新) 第一节:.Net版基于WebSocket的聊天室样例 第十五节:深入理解async和await的作用及各种适用场景和用法 第十五节:深入理解async和await的作用及各种适用场景和用法 前端自动化准备和详细配置(NVM、NPM/CNPM、NodeJs、NRM、WebPack、Gulp/Grunt、G
code&monkey Ext JS学习第十六天 事件机制event(一) 此文用来记录学习笔记: 休息了好几天,从今天开始继续保持更新,鞭策自己学习 今天我们来说一说什么是事件,对于事件 ...
- C#进阶系列 ---- 《CLR via C#》
[C#进阶系列]30 学习总结 [C#进阶系列]29 混合线程同步构造 [C#进阶系列]28 基元线程同步构造 [C#进阶系列]27 I/O限制的异步操作 [C#进阶系列]26 计算限制的异步操作 ...
- C#进阶系列——一步一步封装自己的HtmlHelper组件:BootstrapHelper
前言:之前学习过很多的Bootstrap组件,博主就在脑海里构思:是否可以封装一套自己Bootstrap组件库呢.再加上看到MVC的Razor语法里面直接通过后台方法输出前端控件的方式,于是打算仿照H ...
- C#进阶系列——DDD领域驱动设计初探(一):聚合
前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的.今天看到一篇博文里面写道:越是忙人越有时间写博客.呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的 ...
- C#进阶系列——DDD领域驱动设计初探(二):仓储Repository(上)
前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repository,为什么Repository会有这么大的争议,博主认为主要原 ...
- C#进阶系列——DDD领域驱动设计初探(三):仓储Repository(下)
前言:上篇介绍了下仓储的代码架构示例以及简单分析了仓储了使用优势.本章还是继续来完善下仓储的设计.上章说了,仓储的最主要作用的分离领域层和具体的技术架构,使得领域层更加专注领域逻辑.那么涉及到具体的实 ...
- C#进阶系列——DDD领域驱动设计初探(四):WCF搭建
前言:前面三篇分享了下DDD里面的两个主要特性:聚合和仓储.领域层的搭建基本完成,当然还涉及到领域事件和领域服务的部分,后面再项目搭建的过程中慢慢引入,博主的思路是先将整个架构走通,然后一步一步来添加 ...
- windows程序员进阶系列:《软件调试》之堆 (一)
windows程序员进阶系列:<软件调试>之堆 (一) 堆是软件在运行时动态申请内存空间的主要途径.从堆上申请来的空间需要程序员自己申请和释放,且申请和释放操作必须绝对匹配.忘记释放或者多 ...
- Wireshark入门与进阶系列(一)
摘自http://blog.csdn.net/howeverpf/article/details/40687049 Wireshark入门与进阶系列(一) “君子生非异也,善假于物也”---荀子 本文 ...
随机推荐
- ios UIButton设置单选效果,以及同时设置图片和标题
一,设置单选效果 - (void)selectedBtnPress:(UIButton*)sender { //首先把原来按钮的选中效果消除 for (int i=0;i<num;i++) {/ ...
- Java基础之如何解决斗地主问题
难的是逻辑的分析,把逻辑转化成代码是一种能力,这种能力需要多练习总结. 多多指教,共同进步. 问题: 要求实现斗地主游戏发牌过程,打印三个玩家的牌和底牌.在不看底牌的情况下,统计出三个 ...
- Java程序员的日常 —— 多进程开发
最近再弄进程管理相关的工作,因此必要的就涉及到各种系统下关于进程的管理. 这里简单的介绍下: 如何在Java中执行命令 在windows下肯定是dos命令了,而在linux则为shell命令.执行的方 ...
- Atitit 如何让精灵控件运动
Atitit 如何让精灵控件运动 ##让Sushi精灵动起来 上面的代码,我们创建了静态的sushiSprite,现在我们让它动起来.使它从屏幕顶部下落到屏幕底部.在addSushi方法中添加如下代 ...
- 更新日志 - fir.im Jenkins & Gradle 插件上线
最近 fir.im 工程师们效率爆表,fir.im 实用工具集合又添加了新的成员-- Jenkins & Gradle 插件,让 App 打包上传更加简单快速. fir.im Jenkins ...
- KnockoutJS 3.X API 第四章(14) 绑定语法细节
data-bind绑定语法 Knockout的声明性绑定系统提供了一种简洁而强大的方法来将数据链接到UI. 绑定到简单的数据属性或使用单个绑定通常是容易和明显的. 对于更复杂的绑定,它有助于更好地了解 ...
- HTTP学习三:HTTPS
HTTP学习三:HTTPS 1 HTTP安全问题 HTTP1.0/1.1在网络中是明文传输的,因此会被黑客进行攻击. 1.1 窃取数据 因为HTTP1.0/1.1是明文的,黑客很容易获得用户的重要数据 ...
- 简单的跨平台c/c++日志记录
CLog.h #include <stdlib.h> #pragma once #ifndef _CLOG #define _CLOG #define CLOG_DEBUG 0 #defi ...
- 【WP 8.1开发】同时更新多种磁贴
一般应用程序都会包含多个尺寸的磁贴,如小磁贴(71×71).中磁贴(150×150)和宽磁贴(310×150).常规的磁贴更新做法是用XML文档来定义更新内容,然后再提交更新.如: <tile& ...
- Parallel并行化编程
在很多场景中我们需要通过并行化的方式来提高程序运行的速度,比较典型的需求就是并行下载.前期遇到一个需求是要批量下载瓦片,每次大概下载上百万个瓦片,要想提高瓦片的下载速度,只能通过并行化的方式,下面把我 ...