17.1 什么是泛型
专门为多段代码在不同的数据类型上执行相同指令的情况专门设计
17.2 C#中的泛型
泛型特性提供了一种更优雅的方式,可以让多个类型共享一组代码。泛型允许我们声明类型参数化的代码,可以用不同的类型进行实例化。即可以用 类型占位符 来写代码,然后在创建类的实例时指明真实的类型。
- 类型不是对象而是对象的模板
- 泛型类型不是类型而是类型的模板
C# 提供了5种泛型:
- 类(class)
- 结构(struct)
- 接口(interface)
- 委托(delegate)
- 方法(method)
前4个是类型,而方法是成员
17.3 泛型类
创建和使用常规的、非泛型的类需两步:声明类和创建类的实例
泛型类不是实际的类,而是模板,须先从他们构建实际的类类型,然后创建这个构建后的类类型的实例
- 在某些类型上使用占位符来声明一个类
- 为占位符提供真实类型。这样就有了真实类的定义,填补了所有“空缺”。该类型称为构造类型
- 创建构造类型的实例
17.4 声明泛型类
声明一个简单的泛型类和声明普通类差不多,区别如下
- 在类名之后放置一组尖括号
- 在尖括号中用逗号分隔的占位符字符串来表示希望提供的类型。这叫做 类型参数
- 在泛型类型声明的主体中使用类型参数来表示应该替代的类型
如下代码声明了一个SomeClass的泛型类。类型参数列在尖括号中,然后当作真实类型在声明的主体中使用
类型参数
↓
class SomeClass < T1, T2 >
{ 通常在这些位置使用类型
↓ ↓
public T1 SomeVar = new T1();
public T2 SomeVar = new T2();
}
在泛型类型声明中并没有特殊的关键字。取而代之的是尖括号中的类型参数列表,它可以区分泛型类与普通类的声明
17.5 创建构造类型
创建了编译类型,需要告诉编译器能使用哪些真实类型来替代占位符(类型参数)。编译器获取这些真实类型并创建构造类型(用来创建真实类对象的模板)。
创建构造类型的语法如下,包括列出类名并在尖括号中提供真实类型来替代类型参数。要替代类型参数的真实类型叫做 类型实参
编译器接受了类型实参并且替换泛型类主体中的相应类型参数,产生了构造类型——从它创建真实类型的实例
类型参数和类型实参的区别:
- 泛型类声明的类型参数用做类型的占位符
-
在创建构造类型时提供真实类型的是类型实参
17.6 创建变量和实例
在创建引用和实例方面,构造类类型的使用和常规类型差不多。如下代码演示了两个类对象的创建。
- 第一行显示了普通非泛型类型对象的创建。
- 第二行代码显示了 SomeClass 泛型类型对象的创建,使用 short 和 int 类型进行实例化。这种形式和上面一行差不多,只不过把普通类型名改为构造类形式。
- 第三行和第二行语法一样,没有在等号两边都列出构造类型,而是使用 var 关键字让编译器使用类型引用。
和非泛型类一样,引用和实例可以分开创建,下图可以看出内存中出现的情况与非泛型类是一样的。
- 泛型类声明下面的第一行在栈上 myInst 分配了一个引用,值是 null
- 第二行在堆上分配实例,并且把引用赋值给变量
可以从同一个泛型类型构建出很多不同的类类型。每一个都有独立的类类型,就好像它们都有独立的非泛型类声明一样。
下面代码是从 SomeClass泛型类创建了两个类型
- 一个类型使用 short 和 int 构造
- 另一个类型使用 int 和 long 构造
17.7 类型参数的约束
只要代码不访问它处理的一些类型的对象(或者只要它始终是 object 类型的成员,包括ToString、Equal、GetType等),泛型类就可以处理任何类型。符合约束的类型参数叫做 未绑定的类型参数。然后,如果代码尝试使用其他成员,编译器会产生一个错误信息。
如下代码声明了一个 Simple 的类,有一个 LessThan 的方法,接受两个泛型类型的变量。LessThan尝试用小于运算符返回结果。但由于不是所有的类都实现了小于运算符,也就不能用任何类来代替T,所以编译器会产生一个错误信息。
class Simple<T>
{
static public bool LessThan(T i1, T i2)
{
return i1 < i2; //错误:运算符“<”无法用于“T”和“T”类型的操作符
}
}
要让泛型变得更有用,我们需要提供额外的信息让编译器知道参数可以接受哪些类型。这些额外的信息叫做 约束(constrain)。只有符合约束的类型才能替代给定的类型参数,来产生构造类型。
17.7.1 Where子句
约束使用Where子句列出。
- 每一个有约束的类型参数有自己的 where 子句。
- 如果形参有多个约束,它们在 where 子句中使用逗号分隔。
where子句的语法如下:
类型参数 约束列表
↓ ↓
where TypeParam : constraint, constraint, ...
↑ ↑
关键字 冒号
- 它们在类型参数列表的关闭尖括号之后列出
- 它们不使用逗号或其他符号分隔
- 它们可以以任何次序列出
- where 是上下文关键字,所以可以在其他上下文中使用
如下泛型类有3个类型参数。T1是未绑定,对于T2,只有 Customer 类型或从 Customer 继承的类型的类才能用作类型实参,而对于T3,只有实现 IComparable 接口的类才能用于类型实参。
未绑定 具有约束
↓ ↓ 没有分隔符
class MyClass < T1, T2, T3 > ↓
where T2 : Customer //T2的约束
where T3 : IComparable //T3的约束
{ ↑
... 没有分隔符
}
17.7.2 约束类型和次序
共有5中类型的约束
约束类型 | 描述 |
---|---|
类名 | 只有这个类型的类或从它继承的类才能用作类型实参 |
class | 任何引用类型,包括类、数组、委托和接口都可用作类型实参 |
struct | 任何值类型都可以用作类型实参 |
接口名 | 只有这个接口或实现这个接口的类型才能用作类型实参 |
new() | 任何带有 无参公共构造函数的类型都可作类型实参。叫构造函数约束 |
where 子句可以以任何次序列出,然而,where 子句中的约束必须有特定的顺序
- 最多只能有一个主约束,如果有则必须放在第一位
- 可以有任意多的接口名约束
- 如果存在构造函数约束,则必须放在最后
如下声明给出一个where子句的示例:
17.8 泛型方法
与其他泛型不一样,方法是成员,不是类型。
泛型方法可以在泛型和非泛型的 类、结构和接口 中声明。
17.8.1 声明泛型方法
泛型方法具有类型参数和可选的约束。
- 泛型方法有两个参数列表。
封闭在圆括号内的方法参数列表;
封闭在尖括号内的类型参数列表。 - 要声明泛型方法,需要:
在方法名称之后和方法参数列表之前放置类型参数列表;
在方法参数列表后放置可选的约束子句。
类型参数列表 约束子句
↓ ↓
public void PrintData<S, T> (S p, T t) where S : Person
↑
方法参数列表
注:类型参数列表在方法名称之后,在方法参数列表之前
17.8.2 调用泛型方法
要调用泛型方法,应该在方法调用时提供类型实参:
MyMethod< short, int >();
MyMethod< int, long >();
下图演示了一个 DoStuff 的泛型方法的声明,接受两个类型参数。
推断类型
如果为方法传入参数,编译器有时可以从方法参数中推断出泛型方法的类型形参中用到的那些类型。这样可以使方法调用更简单,可读性更强。
下面代码声明了 MyMethod,它接受了一个与类型参数同类型的方法参数。
public void MyMethod <T> (T myVal) { ... }
↑ ↑
两个都是T类型
如下所示,如果我们使用 int 类型的变量调用 MyMethod,方法调用中的类型参数的信息就多余了,因为编译器可以从方法参数中得知它是 int。
int myInt = 5;
MyMethod <int> (MyInt);
↑ ↑
两个都是int
由于编译器可以从方法参数中推断类型参数,可省略类型参数和调用中的尖括号:
MyMethod(myInt)
17.12 泛型接口
泛型接口允许我们编写参数和接口成员返回类型是泛型类型参数的接口。泛型接口的声明和非泛型接口的声明差不多,但需要在接口名称之后的尖括号中放置类型参数。
下例代码声明了叫做IMyIfc的泛型接口:
- 泛型类 Simple 实现了泛型接口
- Main 实例化了泛型类的两个对象,int 类型和 string 类型
interface IMyIfc<T> //泛型接口
{
T ReturnIt(T inValue);
}
class Simple<S> : IMyIfc<S> //泛型类
{
public S ReturnIt(S inValue) { return inValue; } //实现泛型接口
}
class Program
{
static void Main(string[] args)
{
var trivInt = new Simple<int>();
var trivString = new Simple<string>();
Console.WriteLine("{0}", trivInt.ReturnIt(5));
Console.WriteLine("{0}", trivString.ReturnIt("Hi there"));
}
}
输出如下:
5
Hi there
17.12.1 使用泛型接口的示例
下例演示了泛型接口的两个额外的能力:
- 与其他泛型相似,实现不同类型参数的泛型接口是不同的接口
- 我们可以在非泛型类型中实现泛型接口
下例中 Simple 是实现泛型接口的非泛型类。它实现了两个 IMyIfc的实例。一个实例使用 int 类型实例化,一个使用 string 类型实例化。
interface IMyIfc<T> //泛型接口
{
T ReturnIt(T inValue);
}
class Simple : IMyIfc<int>, IMyIfc<string> //非泛型类
{
public int ReturnIt(int inValue) { return inValue; } //实现 int 类型接口
public string ReturnIt(string inValue) { return inValue; } //实现 string 类型接口
}
class Program
{
static void Main(string[] args)
{
Simple trivial = new Simple();
Console.WriteLine("{0}", trivial.ReturnIt(5));
Console.WriteLine("{0}", trivial.ReturnIt("Hi there"));
}
}
输出结果与上例一致。