泛型概述

泛型(generic)特性提供了一种更优雅的方式,可以让多个类型共享一组代码。
泛型允许我们声明类型参数化(type‑parameterized)的代码,用不同的类型进行实例化。也就是说,可以用 “类型占位符” 来写代码,然后在创建类的实例时在指明真实的类型。
类型不是对象而是对象的模板,同样,泛型类型也不是类型,而是类型的模板。
C#提供了5种泛型:类、结构、接口、委托和方法。前面4个是类型,而方法是成员。

1
2
3
4
5
6
7
8
9
class MyStack<T>
{
int StackPointer = 0;
T [] StackArray;

public void Push(T x ) {//...}
public T Pop() {//...}
//...
}

泛型类

声明泛型类

在泛型类的声明中并没有特殊的关键字。取而代之的是尖括号中的类型参数列表,它可以区分泛型类与普通类的声明。

  • 在类名之后放置一组尖括号。
  • 在尖括号中用逗号分隔的占位符字符串来表示需要提供的类型。这叫作类型参数(type parameter)。
  • 在泛型类声明的主体中使用类型参数来表示替代类型。
1
2
3
4
5
6
7
8
9
10
//                类型参数
// ↓
class SomeClass < T1, T2 >
{// 通常在这些位置使用类型
// ↓
public T1 SomeVar;
public T2 OtherVar;
}
// ↑
//通常在这些位置使用类型

创建构造类型

创建构造类型的语法如下,包括列出类名以及在尖括号中提供真实类型来替代类型参数。替代类型参数的真实类型叫作类型实参(type argument)。

1
2
3
//          类型实参
// ↓
SomeClass< short, int >

创建变量与实例

在创建引用和实例方面,构造类类型的使用和常规类型差不多。

1
2
3
MyNonGenClass myNGC         = new MyNonGenClass();  //普通非泛型类型对象的创建
SomeClass<short, int> mySc1 = new SomeClass<short, int>(); //泛型类型对象的创建
var mySc2 = new SomeClass<short, int>(); //使用var关键字让编译器使用类型引用

和非泛型类一样,引用和实例可以分开创建。

1
2
3
4
5
6
7
8
class SomeClass<T1, T2>
{
public T1 SomeVar;
public T2 OtherVar;
}

SomeClass<short, int> myInst; //分配类变量
myInst = new SomeClass<short, int>(); //分配实例

可以从同一个泛型类型构建出很多不同的类类型。每一个都是独立的类类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SomeClass< T1, T2 >                //泛型类
{
public T1 SomeVar;
public T2 OtherVar;
}

class Program
{
static void Main()
{
var first = new SomeClass<short, int >(); //构造的类型
var second = new SomeClass<int, long>(); //构造的类型
...
}
}

使用泛型的栈的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class MyStack<T>
{
T[] StackArray;
int StackPointer = 0;

public void Push(T x)
{
if (!IsStackFull)
StackArray[StackPointer++] = x;
}

public T Pop()
{
return (!IsStackEmpty)
? StackArray[--StackPointer]
: StackArray[0];
}

const int MaxStack = 10;
bool IsStackFull { get{ return StackPointer >= MaxStack; } }
bool IsStackEmpty { get{ return StackPointer <= 0; } }

public MyStack()
{
StackArray = new T[MaxStack];
}

public void Print()
{
for (int i = StackPointer-1; i >= 0; i--)
Console.WriteLine($" Value: { StackArray[i] }");
}
}

class Program
{
static void Main( )
{
MyStack<int> StackInt = new MyStack<int>();
MyStack<string> StackString = new MyStack<string>();

StackInt.Push(3);
StackInt.Push(5);
StackInt.Push(7);
StackInt.Push(9);
StackInt.Print();

StackString.Push("This is fun");
StackString.Push("Hi there! ");
StackString.Print();
}
}
1
2
3
4
5
6
7
Value: 9
Value: 7
Value: 5
Value: 3

Value: Hi there!
Value: This is fun

类型参数的约束

由于不是所有的类都实现了小于运算符,也就不能用任何类来代替T,所以编译器会产生一个错误消息。

1
2
3
4
5
6
7
8
class Simple<T>
{
static public bool LessThan(T i1, T i2)
{
return i1 < i2; //错误
}
...
}
  • 无约束泛型参数T,编译器只保证拥有object的方法:ToString()、Equals()、GetType()。符合约束的类型参数叫作未绑定的类型参数(unbounded type parameter)。
  • 不能直接对T使用<、>这类运算符,编译器无法确定该类型是否重载了运算符,直接编译报错。
  • 通过泛型约束(constraint),告诉编译器T具备哪些能力,才能调用对应的方法/运算符。

where子句

约束使用where子句列出。

  • 每一个有约束的类型参数都有自己的where子句。
  • 如果形参有多个约束,它们在where子句中使用逗号分隔。
1
2
3
where TypeParam : constraint, constraint, ...
// ↑ ↑ ↑
//关键字 类型参数 约束列表,逗号分隔
  • 它们在类型参数列表的关闭尖括号之后列出。
  • 它们不使用逗号或其他符号分隔。
  • 它们可以以任何次序列出。
  • where是上下文关键字,所以可以在其他上下文中使用。

如下泛型类有3个类型参数。T1是未绑定的类型参数。对于T2,只有Customer类型的类或从Customer派生的类才能用作类型实参。而对于T3,只有实现IComparable接口的类才能用作类型实参。

1
2
3
4
5
6
class MyClass < T1, T2, T3 >   //T1是未绑定的类型参数
where T2: Customer //T2的约束
where T3: IComparable //T3的约束
{
...
}

约束类型和次序

约束类型 描述
类名 只有这个类型的类或从它派生的类才能用作类型实参
class 任何引用类型,包括类、数组、委托和接口都可以用作类型实参
struct 任何值类型都可以用作类型实参
接口名 只有这个接口或实现这个接口的类型才能用作类型实参
new() 任何带有无参公共构造函数的类型都可以用作类型实参。这叫作构造函数约束
where子句可以以任何次序列出。然而,where子句中的约束必须有特定的顺序。
  • 最多只能有一个主约束,而且必须放在第一位。
  • 可以有任意多的接口名称约束。
  • 如果存在构造函数约束,则必须放在最后。
    ![[csharp-15-generic-1.png]]
1
2
3
4
5
6
7
8
9
10
11
class SortedList<S>
where S: IComparable<S> { ... }

class LinkedList<M,N>
where M : IComparable<M>
where N : ICloneable { ... }

class MyDictionary<KeyType, ValueType>
where KeyType : IEnumerable,
new()
{ ... }

泛型方法

方法是成员,不是类型。泛型方法可以在泛型和非泛型的类,结构与接口中声明。

声明泛型方法

泛型方法具有类型参数列表和可选的约束。

  • 泛型方法有两个参数列表。
    • 封闭在圆括号内的方法参数列表。
    • 封闭在尖括号内的类型参数列表。
  • 要声明泛型方法,需要:
    • 在方法名称之后和方法参数列表之前放置类型参数列表;
    • 在方法参数列表后放置可选的约束子句。
1
2
3
4
5
6
7
//<S, T>:类型参数列表
//(S p, T t):方法参数列表
//where S: Person:约束子句
public void PrintData<S, T> (S p, T t) where S: Person
{
...
}

调用泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void DoStuff<T1, T2>( T1 t1, T2 t2 )
{
T1 someVar = t1;
T2 otherVar = t2;
//...
}

DoStuff<short, int>(sVal, iVal);
DoStuff<int, long>(iVal, lVal);

void DoStuff<short,int>(short t1, int t2){
short someVar = t1;
int otherVar = t2;
}
void DoStuff<int,long>(int t1, long t2){
int someVar = t1;
long otherVar = t2;
}

推断类型

如果我们为方法传入参数,编译器有时可以从方法参数的类型中推断出应用作泛型方法的类型参数的类型。这样就可以使方法调用更简单,可读性更强。

1
public void MyMethod <T> (T myVal) { ... }
1
2
int myInt = 5;
MyMethod <int> (myInt);

由于编译器可以从方法参数中推断类型参数,我们可以省略类型参数和调用中的尖括号。

1
MyMethod(myInt);

泛型方法的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Simple                                         //非泛型类
{
static public void ReverseAndPrint<T>(T[] arr) //泛型方法
{
Array.Reverse(arr);
foreach (T item in arr) //使用类型实参 T
Console.Write($"{ item.ToString() }, ");
Console.WriteLine("");
}
}
class Program
{
static void Main()
{
//创建各种类型的数组
var intArray = new int[] { 3, 5, 7, 9, 11 };
var stringArray = new string[] { "first", "second", "third" };
var doubleArray = new double[] { 3.567, 7.891, 2.345 };

Simple.ReverseAndPrint<int>(intArray); //调用方法
Simple.ReverseAndPrint(intArray); //推断类型并调用

Simple.ReverseAndPrint<string>(stringArray); //调用方法
Simple.ReverseAndPrint(stringArray); //推断类型并调用

Simple.ReverseAndPrint<double>(doubleArray); //调用方法
Simple.ReverseAndPrint(doubleArray); //推断类型并调用
}
}
1
2
3
4
5
6
11, 9, 7, 5, 3,
3, 5, 7, 9, 11,
third, second, first,
first, second, third,
2.345, 7.891, 3.567,
3.567, 7.891, 2.345,

扩展方法和泛型类

扩展方法也可以和泛型类结合使用。它允许我们将类中的静态方法关联到不同的泛型类上,还允许像调用类构造实例的实例方法一样来调用方法。
和非泛型类一样,泛型类的扩展方法:

  • 必须声明为 static;
  • 必须是静态类的成员;
  • 第一个参数类型中必须有关键字 this,后面是扩展的泛型类的名字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static class ExtendHolder
{
public static void Print<T>(this Holder<T> h)
{
T[] vals = h.GetValues();
Console.WriteLine($"{ vals[0] },\t{ vals[1] },\t{ vals[2] }");
}
}

class Holder<T>
{
T[] Vals = new T[3];

public Holder(T v0, T v1, T v2)
{
Vals[0] = v0; Vals[1] = v1; Vals[2] = v2;
}

public T[] GetValues() { return Vals; }
}

class Program
{
static void Main(string[] args)
{
var intHolder = new Holder<int>(3, 5, 7);
var stringHolder = new Holder<string>("a1", "b2", "c3");
intHolder.Print();
stringHolder.Print();
}
}
1
2
3,      5,      7
a1, b2, c3

泛型结构

与泛型类相似,泛型结构可以有类型参数和约束。泛型结构的规则和条件与泛型类是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct PieceOfData<T>                     //泛型结构
{
public PieceOfData(T value) { _data = value; }
private T _data;
public T Data
{
get { return _data; }
set { _data = value; }
}
}

class Program
{
static void Main()
{
var intData = new PieceOfData<int>(10);
var stringData = new PieceOfData<string>("Hi there.");

Console.WriteLine($"intData = { intData.Data }");
Console.WriteLine($"stringData = { stringData.Data }");
}
}
1
2
intData    = 10
stringData = Hi there.

泛型委托

泛型委托和非泛型委托非常相似,不过类型参数决定了能接受什么样的方法。
要声明泛型委托,在委托名称之后、委托参数列表之前的尖括号中放置类型参数列表。

1
2
3
delegate R MyDelegate<T, R>( T value );
// ↑ └───┬───┘ └───┬──┘
// 返回类型 类型参数 委托形参

有两个参数列表:委托形参列表和类型参数列表。
类型参数的范围包括:

  • 返回类型;
  • 形参列表;
  • 约束子句。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
delegate void MyDelegate<T>(T value);             //泛型委托
class Simple
{
static public void PrintString(string s) //方法匹配委托
{
Console.WriteLine( s );
}
static public void PrintUpperString(string s) //方法匹配委托
{
Console.WriteLine($"{ s.ToUpper() }");
}
}
class Program
{
static void Main( )
{
var myDel = //创建委托的实例
new MyDelegate<string>(Simple.PrintString);
myDel += Simple.PrintUpperString; //添加方法

myDel("Hi There."); //调用委托
}
}
1
2
Hi There.
HI THERE.

另一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public delegate TR Func<T1, T2, TR>(T1 p1, T2 p2);  //泛型委托
// ↑ ↑ ↑ ↑ ↑
// 委托返回类型 委托参数类型
class Simple
{
static public string PrintString(int p1, int p2) //方法匹配委托
{
int total = p1 + p2;
return total.ToString();
}
}
class Program
{
static void Main()
{
var myDel = //创建委托实例
new Func<int, int, string>(Simple.PrintString);
Console.WriteLine($"Total: { myDel(15, 13) }"); //调用委托
}
}
1
Total: 28

泛型接口

泛型接口允许我们编写形参和接口成员返回类型是泛型类型参数的接口。
泛型接口的声明和非泛型接口的声明差不多,但是需要在接口名称之后的尖括号中放置类型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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($"{ trivInt.ReturnIt(5) }");
Console.WriteLine($"{ trivString.ReturnIt("Hi there.") }");
}
}
1
2
5
Hi there.

使用泛型接口的示例

  • 与其他泛型相似,用不同类型参数实例化的泛型接口的实例是不同的接口;
  • 可以在非泛型类型中实现泛型接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface IMyIfc<T>                              //泛型接口
{
T ReturnIt(T inValue);
}

//源于同一泛型接口的两个不同接口
class Simple : IMyIfc<int>, IMyIfc<string> //非泛型类
{
public int ReturnIt(int inValue) //实现 int 类型接口
{
return inValue;
}

public string ReturnIt(string inValue) //实现 string 类型接口
{
return inValue;
}
}

class Program
{
static void Main()
{
Simple trivial = new Simple();

Console.WriteLine($"{ trivial.ReturnIt(5) }");
Console.WriteLine($"{ trivial.ReturnIt("Hi there.") }");
}
}
1
2
5
Hi there.

泛型接口的实现必须唯一

实现泛型类型接口时,必须保证类型实参的组合不会在类型中产生两个重复的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IMyIfc<T>
{
T ReturnIt(T inValue);
}
// 两个接口
class Simple<S> : IMyIfc<int>, IMyIfc<S> //错误
{
public int ReturnIt(int inValue) //实现第一个接口
{
return inValue;
}
public S ReturnIt(S inValue) //实现第二个接口
{ //如果它不是 int 类型的,
return inValue; //将和第一个接口一样
}
}

协变和逆变

创建泛型实例时,编译器依据泛型类型声明+类型实参生成构造类型。
开发常见错误:试图把派生类型的委托赋值给基类型委托变量。
引出**可变性(variance)**概念,分为三类:

  1. 协变(covariance)
  2. 逆变(contravariance)
  3. 不变(invariance)

协变

每一个变量都有一种类型,你可以将派生类型的对象赋值给基类型的变量,这叫作赋值兼容性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal
{
public int NumberOfLegs = 4;
}

class Dog : Animal
{
}

class Program
{
static void Main()
{
Animal a1 = new Animal();
Animal a2 = new Dog();

Console.WriteLine($"Number of dog legs: { a2.NumberOfLegs }");
}
}
1
Number of dog legs: 4
  1. 普通类有赋值兼容:Dog派生自Animal,Dog实例可以直接赋值给Animal变量。
  2. 普通泛型委托没有继承关系:Factory<Dog>和Factory<Animal>是两个平级委托类型,不存在派生关系,直接把Factory<Dog>赋值给Factory<Animal>会编译报错。
  3. 协变适用场景:类型参数只作为返回值(输出),委托只产出该类型对象。此时派生类版本的委托,可以赋值给基类版本委托。
  4. 语法:泛型委托类型参数前加 out,标记为协变:delegate T Factory<out T>();
  5. 原理:调用委托得到派生类对象(Dog),而派生类对象完全可以当作基类(Animal)使用,类型安全。

    out 协变,输出返回值,派生委托可以赋值给基类委托。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Animal
{
public int Legs = 4; //基类
}

class Dog : Animal { } //派生类

// Factory委托,未加out,不支持协变
delegate T Factory<T>();

class Program
{
static Dog MakeDog() //匹配Factory委托的方法
{
return new Dog();
}

static void Main()
{
Factory<Dog> dogMaker = MakeDog; //创建委托对象
Factory<Animal> animalMaker = dogMaker; //尝试赋值委托对象,编译报错

Console.WriteLine( animalMaker().Legs.ToString() );
}
}

逆变

  1. 适用场景:泛型委托的类型参数只做方法输入参数,只接收传入,不返回出去。
  2. 默认行为:Action1<Animal> 和 Action1<Dog> 默认是两个独立类型,不能直接赋值。
  3. in 关键字开启逆变:delegate void Action1<in T>(T a); 允许:基类委托 Action1<Animal> 赋值给派生类委托 Action1<Dog>。
  4. 为什么安全 调用 dog1(new Dog()),传给方法的是 Dog(派生类),而方法参数接收的是 Animal(基类);Dog属于Animal,方法可以正常使用,类型安全。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal { public int NumberOfLegs = 4; }
class Dog : Animal { }

class Program
{
// in 关键字:逆变,类型参数用作输入参数
delegate void Action1<in T>( T a );

static void ActOnAnimal( Animal a )
{
Console.WriteLine( a.NumberOfLegs );
}

static void Main( )
{
Action1<Animal> act1 = ActOnAnimal;
Action1<Dog> dog1 = act1;
dog1( new Dog( ) );
}
}
1
4

协变和逆变的不同

Base = 基类,Derived = 派生类,Derived : Base

协变 out T

  1. 约束:out修饰的泛型参数只能做返回值(输出),不能用作方法入参。
  2. 赋值方向:F<Derived>(派生委托)可以赋值给 F<Base>(基类委托)。
  3. 类型安全原理: 委托内部返回Derived派生对象,派生对象同时也是基类Base对象;调用方拿到的是自己期望的基类引用,不会出错。

逆变 in T

  1. 约束:in修饰的泛型参数只能做输入参数,不能用作返回值。
  2. 赋值方向:F<Base>(基类委托)可以赋值给 F<Derived>(派生委托)。
  3. 类型安全原理: 调用方传入Derived派生对象,被调用的方法只接收基类Base;派生类对象可以安全传给接收基类的方法,可以正常访问基类成员。
关键字 用途 允许赋值方向 限制
out T 协变 输出 / 返回值 派生委托 → 基类委托 T 只能往外返回,不能当参数
in T 逆变 输入 / 方法参数 基类委托 → 派生委托 T 只能当传入参数,不能返回

out 协变,子赋父(派生赋值给基类);in 逆变,父赋子(基类赋值给派生)。

⚠️注意:不加in/out,两个泛型委托完全独立,互相不能赋值。协变逆变只发生在有继承关系的引用类型。

接口的协变和逆变

  1. 协变、逆变不只用于委托,泛型接口同样支持 out、in。
  2. out T 协变接口:泛型参数 T只能用作方法返回值,不能作为输入参数。 允许:IMyIfc<Dog>(派生类型接口)赋值给 IMyIfc<Animal>(基类接口)。
  3. 原理:GetFirst() 返回 Dog 对象,Dog 继承自 Animal;接收方拿到 Animal 引用,访问基类成员完全类型安全。
  4. 语法约束:接口标记 out T,接口里面所有成员,T 只允许向外输出,不能当入参,编译器强制校验。

拓展对照:接口也可以用in T做逆变,此时 T 只允许做输入参数,支持基类接口赋值给派生类接口,逻辑和委托完全一致。

表格

修饰符 泛型接口 允许赋值 限制
out T 协变接口 派生接口 → 基类接口 T 仅做返回值输出
in T 逆变接口 基类接口 → 派生接口 T 仅做方法输入参数

口诀和委托完全一样:out 协变子赋父,in 逆变父赋子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Animal { public string Name; }
class Dog: Animal { }

// out:协变接口,T只能作为返回值输出
interface IMyIfc<out T>
{
T GetFirst();
}

class SimpleReturn<T>: IMyIfc<T>
{
public T[] items = new T[2];
public T GetFirst() { return items[0]; }
}

class Program
{
static void DoSomething(IMyIfc<Animal> returner)
{
Console.WriteLine( returner.GetFirst().Name );
}

static void Main( )
{
SimpleReturn<Dog> dogReturner = new SimpleReturn<Dog>();
dogReturner.items[0] = new Dog() { Name = "Avonlea" };

// 协变:IMyIfc<Dog> 赋值给 IMyIfc<Animal>
IMyIfc<Animal> animalReturner = dogReturner;

DoSomething(dogReturner);
}
}
1
Avonlea

可变性的更多内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal { public int Legs = 4; }    //基类
class Dog : Animal { } //派生类

class Program
{
// out:协变委托
delegate T Factory<out T>();

static Dog MakeDog() { return new Dog(); }

static void Main()
{
Factory<Animal> animalMaker1 = MakeDog; //隐式强制转换

Factory<Dog> dogMaker = MakeDog;
Factory<Animal> animalMaker2 = dogMaker; //需要 out 标识符

Factory<Animal> animalMaker3
= new Factory<Dog>(MakeDog); //需要 out 标识符
}
}

可变性重要约束

  1. 协变 / 逆变只作用于引用类型;值类型没有继承,不支持。
  2. in / out只能写在委托、接口的泛型参数上;类、结构体、普通方法不能加。
  3. 泛型参数不加in/out = 不变,既不能协变也不能逆变。
  4. 同一个委托 / 接口可以同时拥有:out协变参数、in逆变参数、无修饰不变参数。
1
2
3
delegate T Factory<out R, in S, T>();
// ↑ ↑ ↑ ↑
// 返回值 协变 逆变 不变