深入理解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> ( 一致性转换)
无效:Sample<string>

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);
    }
}
View Code

现在我们有了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]。也就是说,编译时还处于
开放状态的类型,执行时就可能是封闭的。

Type.IsGenericType

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”),并在返回的 所有方法中查找合适的那一个。

IsGenericMethod

3.5  泛型在C#和其他语言中的限制

为什么不能将List<string> 转换成List<object> ?

3.5.1  泛型可变性的缺乏

数组的协变性——引用类型的数组可以被视为它的基类型的数组,或者被视为它所实现的任何接口的数组。

协变性和逆变性统称为可变性。

不变体(invariant )——不支持可变性。

1. 泛型为何不支持协变性

有效(在编译时)  无  效 

Animal[] animals = new Cat[5];
animals[0] = new Turtle();

List<Animal> animals = new List<Cat>();
animals.Add(new Turtle());

在左侧的代码中,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++ 模板的对比

Templates and Generics

3.5.5 和Java泛型的对比

Java 泛型提供了一些出色的特性:

 运行时虚拟机不知道关于泛型的一切,所以只要没有使用旧版本中不存在的类或方法,那么即使在代码中使用了泛型,编译之后代码一样可以在旧版本上运行。

 不需要学习一套新的类就可以使用Java 泛型。非泛型开发者仍然使用ArrayList,泛型开发员只需使用ArrayList<E>。现有的类可以轻松地“升级”到泛型版本。

 以前的特性通过反射系统被有效地利用——java.lang.Class(System.Type 的等价物)是泛型,它允许对编译时类型安全性进行扩展,以覆盖涉及反射的许多情形。然而,在其他一些情况下,它也会带来不便。

 Java 使用通配符来支持协变性和逆变性。例如,ArrayList<? extends Base>可以理解成:“这是从Base派生的某个类型的ArrayList,但我们不知道确切是什么类型。”

3.6 小结

泛型的3个主要优点:编译时的类型安全性、性能和代码的表现力。

posted @ 2019-10-10 20:42  FH1004322  阅读(381)  评论(0)    收藏  举报