学习了类型,可以知道类型的实质是对象的模板,而 泛型 实质就是类型的模板。
泛型允许声明 类型参数化 的代码,即可以使用 类型占位符 来定义泛型,然后在创建类的实例是指明真实的类型。
C#提供五种泛型:
- 类
- 结构
- 委托
- 接口
- 方法
前四种是类型,而方法是成员。
1 泛型类
创建和使用 非泛型 类有两个步骤:
- 声明类
- 创建类型的实例
而使用泛型这需要多一个步骤,就是使用泛型 构建 实际的 类型,构建之后的类型称为 构造类型,然后就像使用非泛型一样,创建这个构建后的类型的实例。
1.1 声明泛型类
声明一个简单的泛型类与声明非泛型类差不多,区别是:
- 在类名后紧跟一组尖括号
- 尖括号内放置 占位符字符串,称为 类型参数
- 在泛型主体中使用 类型参数
例如:
class SomeClass <T1, T2>
{
public T1 SomeVar = new T1();
public T2 OtherVar = new T2();
}
1.2 创建构造类型
创建 构造类型 很简单,只需提供真实类型替代在尖括号中的 类型参数,而这些真实类型又称为 类型实参。
1.3 创建变量和实例
泛型创建实例的形式和非泛型类几乎一样,例如:
// 非泛型
MyNonGenClass myNGC = new MyNonGenClass();
// 泛型
SomeClass<short, int> mySc1 = new SomeClass<short, int>();
var mySc2 = new SomeClass<short, int>(); // 同样可以使用 var 关键字
SomeClass<short, int> mySc3;
mySc3 = new SomeClass<short, int>(); // 和非泛型一样,引用和实例可以分开创建
1.4 类型参数的约束
在泛型主体代码中若尝试不是 object
类的成员代码(因为所有类型都派生自 object
类),编译器会产生一个错误信息。
要让泛型变得更有用,就在定义泛型的时候需要提供额外的信息让编译器知道参数可以接受哪些类型,这些额外的信息就叫做 约束。
1.4.1 where
子句
约束使用 where
子句表示:
where
子句在 类型参数列表 的关闭尖括号之后列出- 每个有约束的 类型参数 都有自己的
where
子句 - 如果一个形参有多个约束,它们在
where
子句中使用 逗号 分隔 - 不同形参的
where
子句不使用任何符号分隔
例如:
class MyClass <T1, T2>
where T2: int
where T1: string, int
{
....
}
// where 子句不需要使用分隔符
约束类型共有五种:
<类名>
:只有这个类型或其派生类可以作为类型实参,是主约束class
:任何引用类型,即类、数组、委托和接口都可以作为类型实参,是主约束struct
:任何值类型都可以作为类型实参,是主约束<接口名>
:只有这个接口或实现这个接口的类型才能用作 类型实参new()
:任何带有无参公共构造函数的类型都可以作为 类型实参,也叫 构造函数约束
其中 where
子句最多只能有一个 主约束,且必须放置在第一位;可以有任意多 接口名约束;如果存在 构造函数约束,这必须放置在最后。
2 泛型方法
与其他泛型不一样,方法是成员不是类型,所以泛型方法可以在以下情况下声明:
- 泛型
- 非泛型
- 结构
- 接口
2.1 声明泛型方法
泛型方法的声明有两个参数列表:
- 类型参数列表,放置在 方法名称 之后
- 方法参数列表,放置在 类型参数列表 之后(方法参数列表后可紧接 约束子句)
例如:
public void PrintData<S, T> (S p, T t) where S: Person
{
...
}
同样的,泛型方法也可以运用到 扩展方法 中
3 泛型委托
定义泛型委托有点要注意的就是 类型参数的范围 了,与泛型方法相似,类型参数 位于委托名称之后,委托参数列表之前的尖括号中。
其中委托的 类型参数 的内容可以包括:
- 形参列表
- 返回值类型
例如:
public delegate TrR Func<T1, T2, TR>(T1 p1, T2 p2); // 其中 T1,T2 为形参,而 TR 为返回值类型
4 泛型接口
创建泛型接口与非泛型接口类似,都是声明接口,然后实现接口,例如:
// 声明接口
interface IMyIfc<T>
{
T ReturnIt(T inValue);
}
// 实现接口
class Simple<S>: IMyIfc<S>
{
public S ReturnIt(S inValue)
{
return inValue;
}
}
class Program
{
static void Main()
{
var trivInt = new Simple<int>();
var trivString = new Simple<string>();
Console.WriteLine("{0}", trivInt.ReturnIt(5));
Console.WriteLine("{0}", trivString.ReturnIt("Hi there"));
}
}
注意:
- 源于同一泛型接口可以构建出不同的接口
- 泛型接口的名字不会和非泛型的冲突
- 实现泛型接口是,必须保证类型实参组合不会再类型中产生两个重复的接口,如
calss Simple<s>: IMyIfc<int>, IMyIfc<S>
这样是不允许的,因为不知道具体的S
值,会导致一个潜在的冲突
5 协变与逆变
协变 out
与逆变 in
只适合用于委托和接口,其实现都是根据 赋值的兼容性。
out
的实现就是因为:调用代码 获得 一个指向其基类引用(派生类 => 基类)
in
的实现就是因为:被调用代码 收到 一个基类引用(基类 => 派生类)