文章目录
- 18.1 什么是泛型
- 18.2 C# 中的泛型
- 18.3 泛型类
- 18.3.1 声明泛型类
- 18.3.2 创建构造类型
- 18.3.3 创建变量和实例
- 18.3.4 使用泛型的示例
- 18.3.5 比较泛型和非泛型栈
- 18.4 类型参数的约束
- 18.4.1 Where 子句
- 18.4.2 约束类型和次序
- 18.5 泛型方法
- 18.5.1 声明泛型方法
- 18.5.2 调用泛型方法
- 18.5.3 泛型方法的示例(*)
- 18.6 扩展方法和泛型类
- 18.7 泛型结构
- 18.8 泛型委托
- 18.9 泛型接口
- 18.9.1 使用泛型接口的示例(*)
- 18.9.2 泛型接口的实现必须唯一
- 18.10 协变和逆变
- 18.10.1 协变(out)
- 18.10.2 逆变
- 18.10.3 协变和逆变的不同
- 18.10.4 接口的协变和逆变
- 18.10.5 关于可变性的更多内容
18.1 什么是泛型
泛型可以将重构代码并且额外添加一个抽象层,是专门为多段代码在不同的数据类型上执行相同指令而设计的。
18.2 C# 中的泛型
泛型不是类型,而是类型的模板。
C# 提供了以下 5 种泛型:
- 类
- 结构
- 接口
- 委托
- 方法
其中 1 ~ 4 是类型,5 是成员。
18.3 泛型类
泛型类不是实际的类,而是类的模板,因此必须先从它们构建实际的类,然后创建类的引用和实例。
- 在某些类型上使用一个占位符来声明一个类。
- 为占位符提供真实类型(构造类型)。
- 创建构造类型的实例。
18.3.1 声明泛型类
- 在类名之后放置一组尖括号。
- 在尖括号中用逗号分隔占位符字符串,用于表示需要提供的类型(类型参数)。
- 在泛型类声明的主体中使用类型参数来表示替代类型。
18.3.2 创建构造类型
声明泛型类后,就可以告诉编译器使用哪些真实类型来替代占位符,编译器将获取这些真实类型并创建构造类型(用来创建真实类对象的模板)。
- 泛型类声明上的类型参数用作类型的占位符。
- 在创建构造类型时提供的真实类型是类型实参。
18.3.3 创建变量和实例
和非泛型类一样,引用和实例可以分开创建。
18.3.4 使用泛型的示例
18.3.5 比较泛型和非泛型栈
18.4 类型参数的约束
要让泛型更加有用,需要提供额外的信息让编译器直到参数可以接受哪些类型,这些额外的信息称为约束。
18.4.1 Where 子句
- 每个有约束的类型参数都有自己的 where 子句。
- 如果形参有多个约束,则使用逗号分隔。
有关 where 子句的要点如下:
- 在类型参数列表的关闭尖括号后列出。
- 不使用分隔符。
- 可以随意次序列出。
- where 是上下文关键字,可以在其他上下文使用。
18.4.2 约束类型和次序
- 最多只能有一个主约束,必须放在第一位。
- 可以有任意个接口名称约束。
- 如果存在构造函数约束,必须放在最后。
18.5 泛型方法
泛型方法可以在泛型 / 非泛型类、结构和接口中声明。
18.5.1 声明泛型方法
- 泛型方法有两个参数列表。
- 方法参数列表(圆括号内)。
- 类型参数列表(尖括号内)。
- 方法参数列表后放置可选的约束子句。
18.5.2 调用泛型方法
编译器使用每个构造函数实例产生方法的不同版本。
编译器有时可以从方法参数推断类型参数。例如,对于如下的方法声明:
编译器可以从 myInt 参数的类型推断出 T 为 int,因此可以省略尖括号。
18.5.3 泛型方法的示例(*)
18.6 扩展方法和泛型类
和非泛型类一样,泛型类的扩展方法必须满足如下条件:
- 声明为 static。
- 是静态类的成员。
- 第一个参数类型中必须有关键字 this,后面是扩展的泛型类的名字。
18.7 泛型结构
泛型结构的规则和条件与泛型类一致。
18.8 泛型委托
C# LINQ 特性大量使用泛型委托。
18.9 泛型接口
泛型接口的声明和非泛型接口的声明类似,但是要在接口名称后的尖括号中放置类型参数。
18.9.1 使用泛型接口的示例(*)
18.9.2 泛型接口的实现必须唯一
必须保证类型实参的组合不会在类型中产生两个重复的接口。
例如,对于下面的泛型接口,会产生潜在的冲突:S 可能用作 int 类型,此时会有两个相同类型的接口,这将不被允许。
- 泛型结构的名称不会和非泛型冲突。
18.10 协变和逆变
18.10.1 协变(out)
给出如下例子:
我么知道,Dog
类型的变量可以作为 Animal
类型的引用,因为 Dog
由 Animal
派生而来,这里发生了隐式类型转换。
进行扩展,添加 Factory 泛型委托、MakeDog 方法,并且 MakeDog 方法可以匹配 Factory 委托。
Main 函数的第二行尝试将 Factory<Dog>
类型赋给 Factory<Animal>
类型,这将产生报错。
问题的原因在于,委托 Factory<Dog>
并没有从 Factory<Animal>
派生得到。
我们仅希望传递 Dog
给 Factory<Animal>
委托时,代码对 Dog
类型中的 Animal
部分进行操作,这并不会发生越界访问,是完全合理的。为了完成我们的期望,可以通过添加 out 关键字改变委托声明。
18.10.2 逆变
与协变相反,如果类型参数只用于方法中的输入参数,那么可以传入更高程度的派生类引用,因为委托的方法中只对其基类部分进行操作。
调用委托时,调用代码为方法 ActOnAnimal 传入的 Dog 类型的变量,而其期望的是 Animal 对象,因此可以进行操作。
18.10.3 协变和逆变的不同
18.10.4 接口的协变和逆变
相同的原则也适用于接口。
18.10.5 关于可变性的更多内容
前面的内容讲解了显式的协变和逆变。实际上,编译器可以自动识别某个已构建的委托是协变还是逆变,并且自动进行类型强制转换,但这通常发生在没有为对象的类型赋值的时候。
- Main 第一行创建了
Factory<Animal>
类型的委托,并直接将方法 MakeDog 赋值给它。由于没有创建Factory<Dog>
委托,因此编译器清楚这是协变关系,允许这种赋值,哪怕委托中没有 out 标识符。 - 到 Main 第三行时,由于第二行已经创建了
Factory<Dog>
委托,因此后面的协变关系赋值需要 out 标识符才能完成。
- 可变性只适用于引用类型,不使用与值类型。
- in、out 关键字的显式变化只适用于委托和接口,不适用于类、结构和方法。
- 不使用 int、out 关键字的委托和接口类型参数是不变的。