第十二章 泛型

2014-06-15

初始泛型

12.3 泛型基础结构

12.3.1 开放类型与封闭类型

12.3.2 泛型类型和继承

12.3.3 泛型类型同一性

12.3.4 代码爆炸

12.6 委托和接口的逆变和协变泛型类型实参

12.7 泛型方法

12.7.1 泛型方法和类型推断

12.9 可验证性合约束

12.9.1 主要约束

12.9.2 次要约束

12.9.3 构造器约束

参考

初识泛型[1][2]


返回

泛型(generic)是CLR和编程语言提供一种特殊机制,它支持另一种形式的代码重用,即"算法重用"。

简单地说,开发人员先定义好一个算法,比如排序、搜索、交换等。但是定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。然后,另一个开发人员只要指定了算法要操作的具体数据类型,就可以使用这个现成的算法了。

泛型有两种表现形式:泛型类型和泛型方法。

  • 泛型类型:大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。除此之外,CLR还允许创建泛型接口和泛型委托。
  • 泛型方法:方法偶尔也封装有用的算法,所以CLR允许引用类型、值类型或接口中定义泛型方法。

以最常用的FCL中的泛型List<T >为例:

  1. using System.Collections.Generic;
  2.  
  3. namespace Generics
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. List<int> num = new List<int>();
  10. num.Add();
  11. num.Add();
  12. int num1 = num[];
  13. int num2 = num[];
  14. }
  15. }
  16. }

尖括号中的T是不确定的数据类型,叫做类型参数(type parameter),一般规定以字母T开头,可以是TKey, TValue都可以。而调用时指定的具体类型叫做类型实参(type argument)。

类型参数是真实类型的占位符。

  • 类型名List是以“`”加数字结尾的。数字表示类型的元数,也就是需要指定具体类型的参数个数。
  • 泛型是类型安全的。如果用“num.Add("a");”会发生编译错误;
  • 泛型可以提高算法的可重用性,而且从例子中看出int类型并没有进行装箱拆箱操作,相比将所有类型转换为Object的方式而言,提高了程序的性能。
  • 为泛型变量设置默认值时常使用default关键字进行,T temp=default(T)。如果T为引用类型,则temp为null;如果T为值类型,则temp设为0值.

12.3 泛型基础结构[2]


返回

为了是泛型能够工作,Microsoft必须完成以下工作:

  • 创建新的IL指令,使之能够识别类型实参
  • 修改现有元数据表的格式,以便表示具有泛型参数的类型名称和方法
  • 修改各种编程语言(C#等),以支持新的语法,允许开发人员定义个引入泛型类型和方法
  • 修改编译器,使之能生成新的IL指令和修改元数据格式
  • 修改JIT编译器,使之能够处理新的、支持类型实参的IL指令,以便生成正确的本地代码
  • 创建新的反射成员,使开发人员能查询类型和成员,以判断它们是否具有泛型参数。另外,还必须定义新的反射成员,使开发人员能在运行时创建泛型类型和方法定义。
  • 修改调试器以以显示和操作泛型类型、成员、字段以及局部变量。
  • 修改VisualStudio 的"智能感知"(IntelliSense)特性。

12.3.1 开放类型与封闭类型

开放类型:具有泛型参数的类型是开放类型,如List<T>,CLR不允许构造开放类型的实例;
封闭类型:在实际调用代码时,如果所有类型实参都已经指定了实际数据类型,如List<string>,则该类型为封闭类型。CLR允许构造封闭类型的实例。

12.3.2 泛型类型和继承

泛型类型仍然是类型,所以它能从其他任何类型派生。使用一个泛型类型并指定类型实参时,实际上是在CLR中定义一个新的类型对象,新的类型对象是从派生该泛型类型的那个类型派生的。也就是说,由于List<T>是从Object派生的,那么List<String>和List<Guid>也是从Object派生的。

12.3.3 泛型类型同一性

有的时候,泛型语法会将开发人员搞糊涂,所以有的开发人员定义了一个新的非泛型类类型,它从一个泛型类型派生,并指定了所有的类型实参。例如,为了简化一下代码:

  1. List<DateTime> dt = new List<DateTime>();

一些开发人员可能首先定义下面这样的一个类:

  1. internal sealed class DateTimeList List<DataTime>
  2. {
  3. //这里无需放任何代码!
  4. }

然后就比较一下DateTimeList和List<DateTime>的同一性:

  1. static void Main(string[] args)
  2. {
  3. DateTimeList dt = new DateTimeList();
  4. Boolean sameType = (typeof(List<DateTime>) == (typeof(DateTimeList)));
  5. }

上述代码运行时,sameType会初始化为false,因为比较的是两个不同类型的对象。

因此,决不要单纯处于增强源代码的易读性类这样定义一个新类。这样会丧失类型同一性(identity)和相等性(equivalence)。也就是说,假如一个方法的原型接受一个DateTimeList,那么不能将一个List<DateTime>传给它。然而,如果方法的原型接受一个List<DateTime>,那么可以将一个DateTimeList传给它,因为DateTimeList是从List<DateTime>派生的。

12.3.4 代码爆炸

使用泛型类型参数的一个方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参进行替换,然后创建恰当的本地代码。然而,这样做有一个缺点:CLR要为每种不同的方法/类型组合生成本地代码。我们将这个现象称为"代码爆炸"。它可能造成引用程序集的显著增大,从而影响性能。
 
CLR内建了一些优化措施,能缓解代码爆炸。首先,假如为一个特定的类型实参调用了一个方法,以后再次使用相同的类型实参来调用这个方法,CLR只会为这个方法/类型组合编译一次。所以,如果一个程序集使用List<DateTime>,一个完全不同的程序集也使用List<DateTime>,CLR只会为List<DateTime>编译一次方法。
 
CLR还提供了一个优化措施,它认为所有引用类型实参都是完全相同的,所以代码能够共享。之所以能这样,是因为所有引用类型的实参或变量时间只是执行堆上的对象的指针,而对象指针全部是以相同的方式操作的。
 
但是,假如某个类型实参是值类型,CLR就必须专门为那个值类型生成本地代码。因为值类型的大小不定。即使类型、大小相同,CLR仍然无法共享代码,可能需要用不同的本地CPU指令操作这些值。

12.6 委托和接口的逆变和协变泛型类型实参


返回

委托的每个泛型类型参数都可标记为协变量或者逆变量。泛型类型参数如下:

  • 不变量(invariant)意味着类型参数不能更改。
  • 逆变量(contravariant)意味着泛型参数可以从一个基类更改为该类的派生类。在c#中,用in关键字标记。逆变参数只出现在输入位置,比如作为方法的参数。
  • 协变量(covariant)意味着泛型类型参数可以从一个派生类改为它的基类。在c#中,用out关键字标记。协变参数只出现的输出位置,比如作为方法的返回类型。

协变逆变的问题实际上都源于一个根本的原则: 子类可以向父类隐式转换,但是父类不能向子类隐式转换。

逆变量代码:

  1. using System;
  2. using System.Collections.Generic;
  3.  
  4. namespace Generics
  5. {
  6. class Animal{}
  7. class Dog : Animal{}
  8. class Program
  9. {
  10. public delegate void Feed<in T>(T target);
  11. static public void FeedAnimal(Animal target)
  12. {
  13. Console.WriteLine("Feed Animal");
  14. }
  15.  
  16. static void Main(string[] args)
  17. {
  18. //先声明一个T为Animal的委托
  19. Feed<Animal> feedAnimalMethod = FeedAnimal;
  20. //将T为Animal的委托赋值给T为Dog的委托变量,这是合法的,因为在定义泛型委托时有in关键字,如果把in关键字去掉,编译器会认为不合法
  21. Feed<Dog> feedDogMethod = feedAnimalMethod;
  22. feedDogMethod(new Dog());
  23. }
  24. }
  25. }

上述代码:Feed<in 子> a = Feed<父> b, 该方法的参数是Animal(即父),Dog(即子)可以隐式转换为Animal(即父)。

协变量代码:

  1. using System;
  2. using System.Collections.Generic;
  3.  
  4. namespace Generics
  5. {
  6. class Animal{}
  7. class Dog : Animal{}
  8. class Program
  9. {
  10. public delegate T Get<out T>();
  11. static public Dog GetDog()
  12. {
  13. return new Dog();
  14. }
  15.  
  16. static void Main(string[] args)
  17. {
  18. //先声明一个T为Animal的委托
  19. Get<Dog> GetDogMethod = GetDog;
  20. //并将findDog赋值给findAnimal是合法的,类型T从Dog向Animal转变是协变
  21. Get<Animal> GetAnimalMethod = GetDogMethod;
  22. GetAnimalMethod();
  23. Console.WriteLine(GetAnimalMethod() is Dog);
  24. }
  25. }
  26. }

上述代码:Get<out 父> a = Get<子>,将返回值Gog(即子)赋给Animal(即父)是隐式转换。

综上所述,虽然协变,逆变形式上不同,但都 遵循一个原则:子类可以向父类隐式转换,但是父类不能向子类隐式转换。

  • 协变:Get<out 父> a = Get<子>
  • 逆变:Feed<in 子> a = Feed<父>

若想了解逆变和协变背景和原理,请参考.NET 4.0中的泛型的协变和逆变

若想了解逆变和协变的使用方法,请参考谈谈.Net中的协变和逆变

12.7 泛型方法


返回

12.7.1 泛型方法和类型推断[1]

为了增强可读性,编译器支持类型推断功能,如下代码 Swap(ref s1, ref s2);,省略<String>:

  1. class Program
  2. {
  3. static void Swap<T>(ref T o1, ref T o2)
  4. {
  5. T temp = o1;
  6. o1 = o2;
  7. o2 = temp;
  8. }
  9.  
  10. static void Main(string[] args)
  11. {
  12. int i1 = ;
  13. int i2 = ;
  14. Swap<int>(ref i1, ref i2);
  15. string s1 = "a";
  16. string s2 = "b";
  17. Swap(ref s1, ref s2);
  18. }
  19. }

注意:类型推断时C#使用的是变量的数据类型,而不是变量引用的对象的类型,如下图:

12.9 可验证性合约束


返回

12.9.1 主要约束

一个类型参数可以指定0或1个主要约束,主要约束可以一个非密封的引用类型,它表示类型实参必须与约束类型相同或者为约束类型的派生类。该引用类型不能为Object, Array, Delegate, MulticastDelegate, ValueType, Enum, Void。

  1. class Constraint1<T> where T : Stream
  2. {
  3. public void Close(T stream)
  4. {
  5. stream.Close();
  6. }
  7. }
  8.  
  9. class Program
  10. {
  11. static void Main(string[] args)
  12. {
  13. Constraint1<FileStream> s2 = new Constraint1<FileStream>();
  14. }
  15. }

两种特殊的主要约束:classstruct

  • class约束:要求指定的类型实参必须是引用类型。Where T:class
  1. class ConstraintClass<T> where T : class
  2. {
  3. void M()
  4. {
  5. T temp = null;
  6. }
  7. }

上述代码,将temp设置为T是合法的,因为T是一个引用类型。C在没有约束的情况下,如果T为值类型,是不能赋值为null的,会产生编译错误。

  • Struct 约束:要求指定的类型实参必须是值类型。Where T:struct
  1. class ConstraintStruct<T> where T : struct
  2. {
  3. static T M()
  4. {
  5. //允许。因为所有值类型都隐式有一个公共无参构造函数
  6. return new T();
  7. }
  8. }

12.9.2 次要约束

一个类型参数可以指定0或者多个次要约束。常见的次要约束主要有两种:

  • 接口约束:类型实参必须实现了指定的所有接口。
  • 类型参数约束:在指定的类型实参之间,存在着一定关系。例如要求存在继承关系
  1. static void M<TBase,T>(TBase tb, T t) where T : TBase {}
  2. static void Main(string[] args)
  3. {
  4. M<Father,Son>(new Father(), new Son()); //comple pass
  5. M<Father, Father>(new Father(), new Father()); //comple pass:
  6. //Son会隐式转换成Father
  7. M<Father, Father>(new Son(), new Father()); //comple pass:
  8. //GradeSon会隐式转换成Father
  9. M<Father,Son>(new GradeSun(), new Son()); //comple pass:
  10.  
  11. //comple ERROR:
  12. //参数 2: 无法从“Generics.Father”转换为“Generics.Son”
  13. M<Father, Son>(new GradeSun(), new Father());
  14.  
  15. //虽然(new Son(), new GradeSun())是继承关系,
  16. //但Complie ERROR:
  17. //不能将类型“Generics.Father”用作泛型类型或方法“Generics.Program.M<TBase,T>(TBase, T)”中的类型形参“T”。
  18. //没有从“Generics.Father”到“Generics.Son”的隐式引用转换。
  19. M<Son, Father>(new Son(), new GradeSun());
  20. }

从上述代码中,我们可以看到:类型参数约束是约束<,>中两者关系,而非(,)中国两者关系。

另外,13行代码错误的原因是无法将 new Father() 转换为 <Son>

12.9.3 构造器约束

  • 构造器约束要求类型实参必须实现了无参构造器,而且它不支持有参构造器。
  1. class ClassA { }
  2. class ClassB
  3. {
  4. public ClassB() { }
  5. }
  6. class ClassC
  7. {
  8. public ClassC(string str) { }
  9. }
  10.  
  11. class Program
  12. {
  13. static T Create<T>()where T:new()
  14. {
  15. return new T();
  16. }
  17.  
  18. static void Main(string[] args)
  19. {
  20. ClassA a = Create<ClassA>(); //compile pass
  21. ClassB b = Create<ClassB>(); //compile pass
  22. ClassC c = Create<ClassC>(""); //Compile ERROR,有参构造器
  23. int d = Create<int>(); //compile pass, 值类型有默认无参构造器
  24. }
  25. }

参考

[1] 跟小静读CLR via C#(16)--泛型 http://www.cnblogs.com/janes/archive/2011/12/21/2295959.html
[2] 《CLR via C#》读书笔记(7) -- 泛型(上) http://www.cnblogs.com/Code-life/archive/2012/12/20/2823520.html

《CLR via C#》读书笔记 之 泛型的更多相关文章

  1. [Clr via C#读书笔记]Cp12泛型

    Cp12泛型 Generic: 特点 源代码保护 类型安全 清晰代码 更佳性能 Framework中的泛型 System.Collections.Generic; 开放类型,封闭类型:每个封闭类型都有 ...

  2. CLR via c#读书笔记八:泛型

    1.定义泛型类型或方法时,为类型指定的任何变量(比如T)都称为类型参数.使用泛型类型或方法时指定的具体数据类型称为类型实参. 2.System.Collections.Concurrent命名空间提供 ...

  3. CLR via C# 读书笔记---常量、字段、方法和参数

    常量 常量是值从不变化的符号.定义常量符号时,它的值必须能在编译时确定.确定后,编译器将唱两只保存在程序集元数据中.使用const关键字声明常量.由于常量值从不变化,所以常量总是被视为类型定义的一部分 ...

  4. CLR via c#读书笔记九:接口

    1.接口对一组方法签名进行了统一命名.接口还能定义事件.无参属性和有参属性(C#的索引器). 2.c#禁止接口定义任何一种静态成员. 3.C#编译器要求将实现接口的方法标记为public.CLR要求将 ...

  5. CLR via #C读书笔记三:基元类型、引用类型和值类型

    1.一些开发人员说应用程序在32位操作系统上运行,int代表32位整数:在64位操作系统上运行,int代表64位整数.这个说法是完全错误的.C#的int始终映射到System.Int32,所以不管在什 ...

  6. Effective Java 读书笔记之四 泛型

    泛型的本质是参数化类型.只对编译器有效. 一.请不要在新代码中使用原生态类型 1.泛型类和接口统称为泛型,有一个对应的原生态类型. 2.原生类型的存在是为了移植兼容性. 3.无限制通配类型和原生态类型 ...

  7. Clr Via C#读书笔记---I/O限制的异步操作

    widows如何执行I/O操作      构造调用一个FileStream对象打开一个磁盘文件-----FileStream.Read方法从文件中读取数据(此时线程从托管代码转为本地/用户模式代码)- ...

  8. Clr Via C#读书笔记---计算限制的异步操作

    线程池基础 1,线程的创建和销毁是一个昂贵的操作,线程调度以及上下文切换耗费时间和内存资源. 2,线程池是一个线程集合,供应你的用程序使用. 3,每个CLR有一个自己的线程池,线程池由CLR控制的所有 ...

  9. Clr Via C#读书笔记---CLR寄宿和应用程序域

    #1 CLR寄宿: 开发CLR时,Microsoft实际是将他实现成包含在一个dll中的COM服务器.Microsoft为CLR定义了一个标准的COM接口,并为该接口和COM服务器分配了GUID.安装 ...

随机推荐

  1. Jmeter脚本录制方法(二)手工编写脚本(jmeter与fiddler结合使用)

    jmeter脚本录制方法可以分三种,前几天写的一篇文章中,已介绍了前两种,今天来说下第三种,手工编写脚本,建议使用这一种方法,虽然写的过程有点繁琐,但调试脚本比前两者方式都要便捷. 首先来看下三种方式 ...

  2. go语言学习-结构体

    结构体 go语言中的结构体,是一种复合类型,有一组属性构成,这些属性被称为字段.结构体也是值类型,可以使用new来创建. 定义: type name struct { field1 type1 fie ...

  3. 如何成为java高手

    成为Java高手是每个Java学习者的梦想,但目前Java知识分支众多,我们该如何学习?本文介绍成为Java高手需要注意的25个学习目标,希望对正在成为Java高手的您有所帮助. 1.你需要精通面向对 ...

  4. [环境]vscode中python虚拟环境

    在项目.vscode/settings.json下设置 { "python.pythonPath": "/path/to/python2.7"}

  5. Web大前端面试题-Day11

    86.如何获得高效的数据库逻辑结构? 从关系数据库的表中 删除冗余信息的过程 称为数据规范化, 是得到高效的关系型数据库表的逻辑结构 最好和最容易的方法. 规范化数据时应执行以下操作: 1.将数据库的 ...

  6. Java开发环境安装过程

    IntelliJ IDEA 安装 下载 配置代理信息 JDK 安装 安装JDK,cmd -> java -version 查看是否有java版本信息安装成功会显示版本信息 配置环境变量Path, ...

  7. 打包maven后出现jar包丢失

    http://blog.csdn.net/asdfsfsdgdfgh/article/details/51373222

  8. webstorm安装教程

    之前有些一些破解的,但是独独忘记了安装的这个教程,现在补上:见下: 先来一官方的解释:WebStorm是JetBrains 推出的一款强大的HTML5编辑工具,拥有丰富的代码快速编辑,可以智能的补全代 ...

  9. 5款替代微软Visio的开源免费软件

    提到流程图和图表设计,自然会想到微软出品的Office Visio,它是一款强大的流程图设计工具.Visio并不在Office标准套装中,需要额外付费购买,这可能会带来某些不便.一方面,并不是所有人都 ...

  10. tensorflow之数据读取探究(2)

    tensorflow之tfrecord数据读取 Tensorflow关于TFRecord格式文件的处理.模型的训练的架构为: 1.获取文件列表.创建文件队列:http://blog.csdn.net/ ...