深入理解C#(第3版)-- 【C#2】第3章 用泛型实现参数化类型(学习笔记)
3.1 为什么需要泛型
那些大量使用集合的代码。几乎每次使用foreach都需要隐式的强制转换。使用那些为不同的数据类型而设计的类型,就意味着会有强制转换.
任何API只要将object作为参数类型或返回类型使用,就可能在某个时候涉及强制类型转换。
代码越容易理解,就越不容易写错!
泛型带来的好处非常像静态语言较之动态语言的优点:更好的编译时检查,更多在代码中能直接表现的信息,更多的IDE 支持,更好的性能。
3.2 日常使用的简单泛型
3.2.1 通过例子来学习:泛型字典
Dictionary<TKey,TValue>
3.2.2 泛型类型和类型参数
泛型有两种形式:泛型类型(包括类、接口、委托和结构——没有泛型枚举)和泛型方法。
类型参数是真实类型的占位符。在泛型声明中,类型参数要放在一对尖括号内,并以逗号分隔。
类型实参(type argument )——使用泛型类型或方法时,要用真实的类型代替。
未绑定泛型类型(unbound generic type )——没有为泛型类型参数提供类型实参。
已构造类型(constructed type )——指定了类型实参的泛型类型。
已构造类型可以是开放或封闭的
开放类型(open type )——还包含一个类型参数(例如,作为类型实参之一或数组元素类型)
封闭类型(closed type )——类型的每个部分都是明确的

namespace System.Collections.Generic { public class Dictionary<TKey, TValue> //声明泛型类型 : IEnumerable<KeyValuePair<TKey, TValue>> //实现泛型接口 { public Dictionary() //声明无参构造函数 { ... } public void Add(TKey key, TValue value) //使用类型参数声明方法 { ... } public TValue this[TKey key] { get { ... } set { ... } } public bool ContainsValue(TValue value) { ... } public bool ContainsKey(TKey key) { ... } } }
构造函数不在尖括号中列出类型参数。类型参数从属于类型,而非从属于某个特定的构造函数,所以才会在声明类型时声明。成员(仅限方法)仅在引入新的类型参数时才需要声明。
泛型类型可以重载,只需改变一下类型参数的数量就可以了。
3.2.3 泛型方法和判读泛型声明
T 是在整个类的范围内使用的类型参数。
非泛型类型也可以拥有泛型方法。
3.3 深化与提高
3.3.1 类型约束
约束要放到泛型方法或泛型类型声明的末尾,并由上下文关键字where来引入。
1. 引用类型约束
第一种约束用于确保使用的类型实参是引用类型(它表示成T : class ,且必须是为类型参数指定的第一个约)。类型实参任何类、接口、数组、委托,或者已知是引用类型的另一个类型参数。
struct RefSample<T> where T : class
有效的封闭类型包括:
RefSample<IDisposable>
RefSample<string>
RefSample<int[]>
无效的封闭类型包括:
RefSample<Guid>
RefSample<int>
以这种方式约束了一个类型参数后,可以使用==和!= 来比较引用(包括null)。
2. 值类型约束
这种约束表示成T : struct ,可以确保使用的类型实参是值类型,包括枚举(enums )。但是,它将可空类型排除在外。
class ValSample<T> where T : struct
有效的封闭类型包括:
ValSample<int>
ValSample<FileMode>
无效的封闭类型包括:
ValSample<object>
ValSample<StringBuilder>
System.Enum和System.ValueType 本身都是引用类型,所以不允许作为ValSample的类型实参使用。
类型参数被约束为值类型后,就不允许使用==和!= 进行比较。
3. 构造函数类型约束
构造函数类型约束表示成T : new() ,必须是所有类型参数的最后一个约束,它检查类型实参是否有一个可用于创建类型实例的无参构造函数。这适用于所有值类型;所有没有显式声明构造函数的非静态、非抽象类;所有显式声明了一个公共无参构造函数的非抽象类。
public T CreateInstance<T>() whereT : new() { return new T(); }
CreateInstance<int>() 和CreateInstance<object>() 都是有效的。但是,CreateInstance<string>();是无效的,因为string没有无参构造函数。
4. 转换类型约束
最后(也是最复杂的)一种约束允许你指定另一个类型,类型实参必须可以通过一致性、引用或装箱转换隐式地转换为该类型。
类型参数约束(type parameter constraint )——规定一个类型实参必须可以转换为另一个类型实参。
| 声明 | 已构造类型的例子 |
| class Sample<T> where T : Stream |
有效:Sample<Stream> ( 一致性转换) |
| struct Sample<T> where T : IDisposable | 有效:Sample<SqlConnection> (引用转换) 无效:Sample<StringBuilder> |
| class Sample<T> where T : IComparable<T> | 有效:Sample<int> ( 装箱转换) 无效:Sample<FileInfo> |
| class Sample<T,U> where T : U | 有效:Sample<Stream,IDisposable> (引用转换) 无效:Sample<string,IDisposable> |
可以指定多个接口,但只能指定一个类。
class Sample<T> where T : Stream, IEnumerable<string>, IComparable<int>
但以下声明就有问题了:
class Sample<T> where T : Stream, ArrayList, IComparable<int>
此外,还有一系列限制:指定的类不可以是结构、密封类(比如string)或者以下任何“特殊”类型:
System.Object ;
System.Enum ;
System.ValueType ;
System.Delegate。
5. 组合约束
由于每一个值类型都有一个无参构造函数,所以假如已经有一个值类型约束,就不允许再指定一个构造函数约(但是,如果T 被约束成一个值类型,仍然可以在方法内部使用new T())。
有效:
class Sample<T> where T : class, IDisposable, new() class Sample<T> where T : struct, IDisposable class Sample<T,U> where T : classwhere U : struct, T class Sample<T,U> where T : Stream where U : IDisposable
无效:
class Sample<T> where T : class, struct class Sample<T> where T : Stream, class class Sample<T> where T : new(), Stream class Sample<T> where T : IDisposable, Stream class Sample<T> where T : XmlReader, IComparable, IComparable class Sample<T,U> where T : struct where U : class, T class Sample<T,U> where T : Stream, U : IDisposable
记住,每个类型参数的约束列表都要单独用一个where 引入。
3.3.2 泛型方法类型实参的类型推断
类型推断只适用于泛型方法,不适用于泛型类型。
C# 2 的所有规则,其基本步骤如下:
(1) 对于每一个方法实参(普通圆括号中的参数,而不是尖括号中的),都尝试用十分简单的技术推断出泛型方法的一些类型实参。
(2) 验证步骤(1)的所有结果都是一致的——换言之,假如从一个方法实参推断出了某类型参数的类型实参,但根据另一个方法实参推断出同一个类型参数具有另一个类型实参,则此次方法调用的推断失败。
(3) 验证泛型方法需要的所有类型实参都已被推断出来。不能让编译器推断一部分,自己显式指定另一部分。要么全部推断,要么全部显式指定。
3.3.3 实现泛型
1. 默认值表达式
TryXXX 模式 TryXXX模式的用途在从.NET 1.1 升级到2.0期间进行了扩展。它是针对以下情况设计的:有些错误虽然一般会被视为错误(在这种情况下,方法不能履行其基本职责),但并不是什么严重的问题,也不应该视为异常。
代码清单3-4 以泛型方式将一个给定的值和默认值进行比较
static int CompareToDefault<T>(T value) whereT : IComparable<T> { return value.CompareTo( default(T)); } ... Console.WriteLine(CompareToDefault("x")); Console.WriteLine(CompareToDefault(10)); Console.WriteLine(CompareToDefault(0)); Console.WriteLine(CompareToDefault(-10)); Console.WriteLine(CompareToDefault(DateTime.MinValue));
2. 直接比较
如果一个类型参数是未约束的(即没有对其应用约束),那么且只能在将该类型的值与null进行比较时才能使用==和!=操作符。
泛型比较接口
共有4个主要的泛型接口可用于比较。IComparer<T> 和IComparable<T>用于排序(判断某个值是小于、等于还是大于另一个值),而IEqualityComparer<T> 和
IEquatable<T>通过某种标准来比较两个项的相等性,或查找某个项的散列(通过与相等性概念匹配的方式)。
如果换一种方式来划分这4个接口,IComparaer<T> 和IEqualityComparer<T>的实例能够比较两个不同的值,而IComparable<T>和<TIEquatable<T>的实例则可以比较它们本身和其他值。
3. 完整的比较例子:表示一对值
.NET 4 和元组 System命名空间下的Tuple<T1>、Tuple<T1, T2>
代码清单3-6 表示一对值的泛型类
using System; using System.Collections.Generic; public sealed class Pair<T1, T2> : IEquatable<Pair<T1, T2>> { private static readonly IEqualityComparer<T1> FirstComparer = EqualityComparer<T1>.Default; private static readonly IEqualityComparer<T2> SecondComparer = EqualityComparer<T2>.Default; private readonly T1 first; private readonly T2 second; public Pair(T1 first, T2 second) { this.first = first; this.second = second; } public T1 First { get { return first; } } public T2 Second { get { return second; } } public bool Equals(Pair<T1, T2> other) { return other != null && FirstComparer.Equals(this.First, other.First) && SecondComparer.Equals(this.Second, other.Second); } public override bool Equals(object o) { return Equals(o as Pair<T1, T2>); } public override int GetHashCode() { return FirstComparer.GetHashCode(first) * 37 + SecondComparer.GetHashCode(second); } }
现在我们有了Pair类,该如何构造它的实例呢?这时,你需要使用下面的代码:
Pair<int,string> pair = new Pair<int,string>(10, "value");
这并不十分理想,要是能使用类型推断就好了,但是那只能用于泛型方法,而且Pair类不包含任何泛型方法。
代码清单3-7 使用包含泛型方法的非泛型类型进行类型推断
public static class Pair { public static Pair<T1, T2> Of<T1, T2>(T1 first, T2 second) { return new Pair<T1, T2>(first, second); } }
如果你是第一次阅读本书,请忽略静态类型这个声明,等到第7 章再介绍。重点是我们拥有了一个包含泛型方法的非泛型类。这意味着可以将之前的示例改写为下面这种优雅的形式:
Pair<int,string> pair = Pair.Of(10, "value");
3.4 高级泛型
3.4.1 静态字段和静态构造函数
如果在SomeClass中声明了静态字段x,不管创建SomeClass的多少个实例,也不管从SomeClass派生出多少个类型,都只有一个SomeClass.x 字段。这在C# 1 就很常见,那么它与泛型的关系是怎样的呢?
答案是:每个封闭类型都有它自己的静态字段集。
代码清单3-8 证明不同的封闭类型具有不同的静态字段
class TypeWithField<T> { public static string field; public static void PrintField() { Console.WriteLine(field + ": " + typeof(T).Name); } } ... TypeWithField<int>.field = "First"; TypeWithField<string>.field = "Second"; TypeWithField<DateTime>.field = "Third"; TypeWithField<int>.PrintField(); TypeWithField<string>.PrintField(); TypeWithField<DateTime>.PrintField();
代码清单3-8的输出如下:
First: Int32
Second: String
Third: DateTime
基本的规则是:“每个封闭类型有一个静态字段。”同样的规则也适用于静态初始化程序(static initializer)和静态构造函数(static constructor )。
代码清单3-9 嵌套泛型类型的静态构造函数
public class Outer<T> { public class Inner<U, V> { static Inner() { Console.WriteLine("Outer<{0}>.Inner<{1},{2}>", typeof(T).Name, typeof(U).Name, typeof(V).Name); } public static void DummyMethod() {} } } ... Outer<int>.Inner<string, DateTime>.DummyMethod(); Outer<string>.Inner<int, int>.DummyMethod(); Outer<object>.Inner<string, object>.DummyMethod(); Outer<string>.Inner<string, object>.DummyMethod(); Outer<object>.Inner<object, string>.DummyMethod(); Outer<string>.Inner<int, int>.DummyMethod();
每个不同的类型实参列表都被看做一个不同的封闭类型,所以代码清单3-9的输出如下:
Outer<Int32>.Inner<String,DateTime>
Outer<String>.Inner<Int32,Int32>
Outer<Object>.Inner<String,Object>
Outer<String>.Inner<String,Object>
Outer<Object>.Inner<Object,String>
和非泛型类型一样,任何封闭类型的静态构造函数只执行一次。
3.4.2 JIT编译器如何处理泛型
JIT为每个以值类型作为类型实参的封闭类型都创建不同的代码。然而,所有使用引用类型(string、Stream、StringBuilder 等)作为类型实参的封闭类型都共享相同的本地代码。之所以能这样做,是由于所有引用都具有相同的大小(32 位CLR 上是4字节,64 位CLR 上是8字节。但是,在任何一个特定的CLR 中,所有引用都具有相同的大小)。
3.4.3 泛型迭代
在C# 1 中,为了使用foreach,集合要么必须实现System.Collections.IEnumerable 接口,要么必须有一个类似的GetEnumerator()方法,返回的类型含有一个恰当的MoveNext()方法和Current属性。
C# 2使用System.Collections.Generic.IEnumerable<T>接口及其搭档IEnumerator<T>。这意味着在遍历由值类型的元素构成的泛型集合(比如List<int>)时,根本不会执行任何装箱。
3.4.4 反射和泛型
可以用它检查充满各种“程序集”(assembly )的目录,以寻找某个plugin 接口的实现。可以为控制反转框架(参见http://mng.bz/xc3J )编写一个文件,来加载和动态配置应用程序的组件。
1. 对泛型类型使用typeof
typeof 可通过两种方式作用于泛型类型——一种方式是获取泛型类型定义(即“未绑定泛型类型”),另一种方式是获取特定的已构造类型。
为了获取泛型类型定义(即没有指定任何类型实参的类型),需要提供声明的类型名称,删除所有类型参数名称,但保留逗号。为了获取已构造类型,需要采取与声明泛型类型变量时相同的方式指定类型实参就可以了。
代码清单3-11 对类型参数使用typeof操作符
static void DemonstrateTypeof<X>() { Console.WriteLine(typeof(X));//显示方法的类型参 Console.WriteLine(typeof(List<>));//显示泛型类型 Console.WriteLine(typeof(Dictionary<,>)); Console.WriteLine(typeof(List<X>));//显式封闭类型(尽管使用了类型参数) Console.WriteLine(typeof(Dictionary<string, X>)); Console.WriteLine(typeof(List<long>));//显式封闭类型 Console.WriteLine(typeof(Dictionary<long, Guid>)); } ... DemonstrateTypeof<int>();
在IL 中,类型参数的数量是在框架所用的完整类型名称中指定的。在这个完整类型名称的第一部分之后,会添加一个` 字符,然后是参数数量。
注意任何使用了方法类型参数(X )的地方,执行时都会使用类型实参的实际值。所以,会打印List`[System.Int32],而非你认为的List`1[X]。也就是说,编译时还处于
开放状态的类型,执行时就可能是封闭的。
2. System.Type的属性和方法
GetGenericTypeDefinition和MakeGenericType。两个方法所执行的操作实际上是相反的——第一个作用于已构造的类型,获取它的泛型类型定义;第二个作用于泛型类型定义,返回一个已构造类型。
.Net 1.1 中就有的Type.GetType(string)以及和它相关的Assembly.GetType(string)方法。
你可能期望对适当的程序集调用GetType方法,可以得到与代码清单3-11 同样的输出结果。可惜,事情并没有这么简单。针对封闭的已构造类型,是可以这样做的——将类型实参放到方括号中即可。但是,对于泛型类型定义,则需要完全删除方括号——否则GetType会认为你指定的是一个数组类型。代码清单3-12 演示了所有这些方法的实际应用。
3. 反射泛型方法
代码清单3-13 通过反射来获取和调用泛型方法
public static void PrintTypeParameter<T>() { Console.WriteLine(typeof(T)); } ... Type type = typeof(Snippet); MethodInfo definition = type.GetMethod("PrintTypeParameter"); MethodInfo constructed = definition.MakeGenericMethod(typeof(string)); constructed.Invoke(null, null);
首先获取泛型方法定义,然后使用MakeGenericMethod返回一个已构造的泛型方法。和类型一样,还可以执行其他操作,但和Type.GetType不同的是,没有办法在GetMethod调用中指定一个已构造的方法。另外,对于只是类型参数数量不同的多个重载方法,.NET Framework 还存在一个问题:在Type中,没有任何方法允许指定类型参数的数量。所以,只能调用Type.GetMethods(注意多了一个“s”),并在返回的 所有方法中查找合适的那一个。
3.5 泛型在C#和其他语言中的限制
为什么不能将List<string> 转换成List<object> ?
3.5.1 泛型可变性的缺乏
数组的协变性——引用类型的数组可以被视为它的基类型的数组,或者被视为它所实现的任何接口的数组。
协变性和逆变性统称为可变性。
不变体(invariant )——不支持可变性。
1. 泛型为何不支持协变性
| 有效(在编译时) | 无 效 |
|
Animal[] animals = new Cat[5]; |
List<Animal> animals = new List<Cat>(); |
在左侧的代码中,animals实际引用的对象是一个Cat[] ;在右侧的代码中,animals实际引用的是一个List<Cat>。两者都只要求存储对Cat 实例的引用。左侧的数组版本虽然可以通过编译,但执行时一样会失败。
泛型的设计者认为,这比编译时就失败还要糟糕——静态类型的全部意义就在于在代码运行之前找出错误。
为什么数组是协变的?
.NET之所以有协变数组,是因为Java 有协变数组
2. 协变性在什么时候有用
(这部分看不太懂!!!)
最明显的例子就是IEnumerator<T>和(相关的)IEnumerable<T>。实际上,它们是泛型协变最典型的示例。它们共同描述一个值的序列,每一个值都与T兼容,因此可以写成这样:
T currentValue = iterator.Current;
这里只使用了普通的兼容概念,例如,一个IEnumerator<Animal>可以产生对Cat 或Turtle 实例的引用。我们无法添加与实际序列类型不符的值,因此希望能够将IEnumerator <Cat>看成是IEnumerator<Animal>。
假设使用自定义的形状示例来描述继承关系,其中包含一个接口(IShape)。现在考虑另一个由形状组成的图形接口IDrawing 。我们拥有两个具体的图形类型——MondrianDrawing(由矩形组成)和SeuratDrawing (由圆形组成)。

两个图形类型都需要实现IDrawing 接口,所以需要公开具有以下签名的属性:
IEnumerable<IShape> Shapes { get; }
然而如果每个图形类型都在内部维护一个更强类型的列表,将会更加简单。例如,Seurat图形可以包含一个List<Circle>类型的字段,这对它来说比List<IShape>要有用得多,因为如果需要以圆特有的方式来操作圆形时,就不必进行强制转换了。如果是List<IShape>,我们可以直接返回或将其包装在ReadOnlyCollection<IShape> 中,以防止调用者通过强制转换破坏其状态——这两种实现都代价低廉且十分简单。但如果类型不匹配,就不能这么做了。我们无法将IEnumerable<Circle>转换为IEnumerable<IShape>。那我们能做些什么呢? 以下是一些可以进行的选择:
将字段类型改为List<IShape>并保留强制转换。但这并不优雅,而且丧失了泛型的很多优势。
使用C# 2 提供的实现迭代器的新特性,第6章将详细介绍。这是一个合理的解决方案,但仅限于处理IEnumerable<T>这种情况。
在Shapes属性的实现中创建列表的新副本,简便起见可以使用List<T>.ConvertAll 。尽管在API中,为集合创建一个独立的副本通常没有任何问题,但在很多情况下,大量的复制会导致不必要的效率降低。
将IDrawing 改为泛型接口,指明图形中的形状类型。因此ModrianDrawing将实现IDrawing<Rectangle>、SeuratDrawing 将实现IDrawing<Circle> 。该方法只有在
你拥有这个接口时才能使用。
创建一个辅助类将IEnumerable<T>适配成另一个:
class EnumerableWrapper<TOriginal, TWrapper> : IEnumerable<TWrapper> whereTOriginal : TWrapper
同样,由于这种情形(IEnumerable<T>)的特殊性,我们可以使用一个工具方法。事实上,.NET 3.5发布了两个有用的方法:Enumerable.Cast<T>和Enumerable.OfType<T> 。
3. 逆变性在什么地方有用
使用协变性,可以将
SomeType<Circle>转换为SomeType<IShape>(上面那个例子中的SomeType 为IEnumerable <T> )。而逆变性则是进行反向转换——从SomeType<IShape>转换为SomeType<Circle>。这怎么可能安全呢?实际上,当SomeType只描述返回类型参数的操作时,协变就是安全的;而当SomeType只描述接受类型参数的操作时,逆变就是安全的。
代码清单3-14 使用泛型辅助类解决逆变性缺乏问题
class ComparisonHelper<TBase, TDerived> : IComparer<TDerived> whereTDerived : TBase //恰当地约束类型参数 { private readonly IComparer<TBase> comparer;//保存原始的比较器 public ComparisonHelper(IComparer<TBase> comparer) { this.comparer = comparer; } public int Compare(TDerived x, TDerived y) { return comparer.Compare(x, y);//使用隐式类型转换来调用比较器 } }
3.5.2 缺乏操作符约束或者“数值”约束
动态特性
3.5.3 缺乏泛型属性、索引器和其他成员类型
前面已讨论了泛型类型(包括类、结构、委托和接口)和泛型方法。还有其他许多成员可以参数化。但是,不存在泛型的属性、索引器、操作符、构造函数、终结器
或事件。
3.5.4 同C++ 模板的对比
3.5.5 和Java泛型的对比
Java 泛型提供了一些出色的特性:
运行时虚拟机不知道关于泛型的一切,所以只要没有使用旧版本中不存在的类或方法,那么即使在代码中使用了泛型,编译之后代码一样可以在旧版本上运行。
不需要学习一套新的类就可以使用Java 泛型。非泛型开发者仍然使用ArrayList,泛型开发员只需使用ArrayList<E>。现有的类可以轻松地“升级”到泛型版本。
以前的特性通过反射系统被有效地利用——java.lang.Class(System.Type 的等价物)是泛型,它允许对编译时类型安全性进行扩展,以覆盖涉及反射的许多情形。然而,在其他一些情况下,它也会带来不便。
Java 使用通配符来支持协变性和逆变性。例如,ArrayList<? extends Base>可以理解成:“这是从Base派生的某个类型的ArrayList,但我们不知道确切是什么类型。”
3.6 小结
泛型的3个主要优点:编译时的类型安全性、性能和代码的表现力。
浙公网安备 33010602011771号