泛型
- 泛型概述
- 提供了一种让多个类型共享一组代码的方式,允许声明类型参数化的代码,可以用不同的类型进行实例化
- 泛型类型不是类型,是类型的模板
- 五种泛型(注意前四个是类型,方法是成员)
- 类
- 结构
- 接口
- 委托
- 方法
- 在类名后面放置
<T>
,然后在类中使用类型占位符T
替换掉原本的类型,结果就是泛型类声明
- 泛型类
- 声明类和创建类的实例
- 泛型不是类而是类的模板,所以必须先从它们构建实际的类类型
- 步骤
- 在某些类型上使用占位符来声明一个类
- 为占位符提供真实类型,这样就有了真实类的定义,该类型称为构造类型
- 创建构造类型的实例
- 声明泛型类
- 在类型后面放置一组尖括号
- 尖括号中用逗号分隔的占位符字符串来表示希望提供的类型,这叫做类型参数
- 在泛型类声明中使用类型参数来表示应该替代的类型
- 创建构造类型
- 告诉编译器能使用哪些真实类型来替代类型参数,编译器获得真实类型并创建构造类型用来创造真实类对象的模板
- 要替代类型参数的真是类型叫做类型实参
- 创建变量和实例
- 非泛型
- 源代码大小:更大,需要为每一中类编写一个新的实现
- 可执行大小:无论每一个版本都会被使用,都会在编译的版本中出现
- 写的难易度:易于书写,因为它更具体
- 维护的难易度:更容易出问题,因为所有的修改需要用到
- 泛型
- 源代码大小:更小,不管构造类型数量有多少,只需要一个实现
- 可执行大小:可执行文件中只会出现有构造类型的类型
- 写的难易度:比较难写,因为它更抽象
- 维护的难易度:易于维护,因为它只需要修改一个地方
- 非泛型
- 类型参数的约束
- 所有的对象最终从object继承,泛型类只能确定这些参数类型实现了object的成员,如果代码尝试使用其他成员,编译器会产生一个错误信息
- 要让泛型变得更有用,需要提供额外信息让编译器知道参数可以接受哪些类型来产生构造类型
- 符合约束的类型参数叫做未绑定的类型参数
- where子句:约束使用where子句列出
- 每一个有约束的类型参数都有自己的where子句
- 如果形参有多个约束,它们的where子句使用逗号分隔
- 语法:
where TypeParam : constraint1, constraint2, ...
- 有关where子句的要点:
- 它们在类型参数列表关闭的尖括号之后列出
- 它们不使用逗号或其他符号分隔
- 它们可以以任何次序列出
- where是上下文关键字,所以可以在其他上下文中使用,即在其他地方可以用where作为变量名等
-
class MyClass<T1, T2, T3> where T2 : Customer where T3 : IComparable { // ... } // 一个 where子句 只能写一个类型,但是可以有多条限制,顺序有要求下面讲 // 多个 where子句 之间,不用分隔符,顺序任意
- 约束类型
- 类名:只有这个类型的类或子类才能作类型实参
- class:任何引用类型,包括类、数组、委托和接口都可以用作类型实参
- struct:任何值类型可以用作实参
- 接口名:只有这个接口或实现这个接口的类型才能用作类型实参
- new():任何带有无参公共构造函数的类型都可以用作实参,这叫做构造函数约束
- 约束的次序
- 最多只能有一个主约束(类名、class、struct),如果有则必须放在第一位
- 可以有任意多的接口名约束
- 如果有构造函数约束,则必须放在最后
- 泛型方法
- 与其他泛型不一样,泛型方法是成员不是类型,可以在非泛型类中声明
- 声明泛型方法
- 方法名称之后和方法参数列表之前放置类型参数列表
- 在方法参数列表后放置可选的约束子句
- 返回值可以是泛型类型
- 调用泛型方法
- 提供类型实参
- 推断类型
- 有时可以从方法参数中推断出泛型方法的类型形参用到的类型
- 如对于方法
public void MyMethod<T> (T myVal) { ... }
- 实际调用时可以写
MyMethod<int>(5)
,也可以省略方法形参直接写MyMethod(5)
- 除了相同类型时,其他有用到T的也可以推断,如类型参数是
T
但参数是T[]
- 扩展方法和泛型类
- 扩展方法(静态方法,第一个参数使用this修饰)可以和泛型类结合使用
- 泛型类的扩展方法
- 必须声明为static
- 必须是静态类成员
- 第一个参数必须有关键字this
- 泛型结构
- 泛型结构的规则和条件与泛型类是一样的
- 泛型委托
- 在委托类型名称和委托参数列表之间加上尖括号放置类型参数列表
- 在方法参数列表后面添加约束子句
- 泛型接口
- 声明类似泛型类
- 实现接口的时候可以用泛型类的类型参数作为泛型接口的类型实参
-
interface IB<T> { // ... } class A<S> : IB<S> { // ... }
- 可以在非泛型类中实现泛型接口
- 实现不同类型参数的泛型接口是不同的接口,如
A
可以同时实现IB<string>
和IB<int>
,但实现具体类型不能再使用泛型,如A<S>
实现不能同时实现IB<int>
和IB<S>
,因为当S为int时存在潜在的冲突,但是泛型接口的名字不会和非泛型接口冲突
-
- 协变和逆变
- 可变性
- 协变、逆变、不变
- 协变
- 原则上父类引用可以指向子类对象,但是对于泛型委托来说,如果一个泛型委托的类型参数只作为输出值,他只能指向相同类型参数的委托类型,而不能指向类型参数为子类的委托类型,即下面的代码会报错
-
class Animal { } class Dog : Animal { } delegate T Factory<T>(); class Program { static Dog MakeDog() { return new Dog(); } static void Main() { Factory<Dog> dogMaker = MakeDog; Factory<Animal> animalMaker = dogMaker; // 报错,因为两个委托之间不存在继承关系 } }
- 如果派生类只用于输出值,这种结构化的委托有效性之间的常数关系叫做协变
- 为了让编译器知道这是我们的期望,则需要用
out
关键字标记委托声明中的类型参数 - 即将上面的代码修改为
delegate T Factory<out T>();
即可通过编译
- 逆变
- 与上面的例子类似
- 期望传入基类时允许传入派生对象的特性叫逆变
-
class Animal { public int t = 10; } class Dog : Animal { } delegate void Act<in T>(T t); class Program { static void ActOnAnimal(Animal a) { System.Console.WriteLine(a.t); } static void Main() { Act<Animal> act = ActOnAnimal; Act<Dog> dog = act; dog(new Dog()); } }
- in为逆变关键字
- 如果类型参数只用作委托方法的输入参数,可以添加逆变关键字in,让两种不兼容但是理论上可行的类型可以成功赋值
- 协变与逆变
- 协变
F<out T>()
类型的委托,类型变量是父类- 实际构建委托时,使用子类的类型变量进行声明
- 在调用的时候,方法返回指向子类对象的引用
- 逆变
F<in T>(T t)
类型的委托,类型参数是子类- 实际构造委托时,使用父类类型进行声明
- 在调用的时候,方法传入子类的变量,参数的父类引用指向子类对象
- 协变
- 接口的协变与逆变
- 接口除了应用到委托上,还可以应用到接口上
- 在泛型接口声明的时候,在泛型类型前加上协变/逆变关键字
- 如果是返回类型和委托不匹配的方法去给委托赋值时,不需要添加out关键字,编译器会自动识别
- 但是如果刚刚的方法已经创建了委托并用该委托去给其他委托赋值时,就需要添加out关键字
- 可变性