[C#]泛型
概述
泛型是一种代码重用的机制。我们知道,面向对象可以重用对象的行为,而泛型是为了重用算法。
C#的引用类型(class)与值类型(struct)都允许使用泛型,但是枚举类型(enum)不可以。C#还可以定义泛型接口(interface)、泛型方法(静态方法与成员方法都可以)、泛型委托(delegate)。
使用泛型封装算法的好处:
- 源代码保护(不太理解,和C++模板有联系)
- 类型安全。非泛型算法,由于类型未知,只能在运行时才会知道类型是否有问题。使用泛型则在编译时就可以将类型问题找出。
- 更清晰的代码
- 更好的性能。非泛型算法可能会带来装箱与拆箱的损耗,而且可以避免强制类型转换。
泛型的结构
开放类型与封闭类型
我们知道,C#的任何一个类型,都有对应类型对象(type object)储存类型的元数据。泛型类型也拥有类型对象,称为开放类型。但是我们不可以构造一个开放类型的实例。
1 |
|
程序会在第7行抛出System.ArgumentException
异常
当我们直接打印一个开放类型的完整名称,例如打印System.Collections.Generic.List<>
,会得到"System.Collections.Generic.List`1"。末尾的"`1"代表有多少个泛型参数。
确定了泛型类型的类型称为封闭类型。
泛型类型内的静态字段
泛型类型内部可以定义静态字段、属性。与普通类型的静态字段全局唯一不同,泛型类型内的任何静态字段不会在泛型类型间共享,它们会被独立出来。
1 |
|
控制台会打印2和3,而不是两个3。
泛型类型的继承
泛型类型也是类型,所以能从任何类型派生。继承泛型类型时指定实参,那么相当于定义了新得类型对象,新类型从填入实参后的泛型类型派生。例如Dictionary<K,V>
派生自Object
,那么Dictionary<int,double>
也派生自Object
。当StrMap<V>
派生自Dictionary<string,V>
,那么StrMap<int>
也派生自Dictionary<string,int>
使用using简化泛型
有时候我们会用一个类来封装泛型
1 |
|
但C#提供了更好的方式,来为泛型类型重命名
1 |
|
使用using语句来定义一个符号代替泛型
泛型方法的背后
一般来说,当我们调用了一个泛型方法并填入想要的泛型参数,我们希望参数就像C/C++的宏一样,将方法内的泛型类型替换成我们填入的参数。但这会导致,为每一个填入的参数都生成一个对应的方法,让方法数量爆炸,影响性能。当然,C#已经解决了这个问题,C#的运行时CLR(Common Language Runtime)如果发现,填入的参数是引用类型,就只会生成一套完全相同的代码,运行时填入实例,因为引用类型可以看作一个指向堆内存的指针,而指针的大小是固定的,所有指针都可以统一操作。但如果填入的是值类型,CLR才会生成独立的方法,因为每一个值类型的大小都不确定,编译器无法确定要使用的栈空间大小。
泛型的协变与逆变
协变与逆变允许将修改类型但泛型参数类型不变
- 逆变:泛型参数可以改变为它的派生类型,在泛型参数前填in
- 协变:泛型参数可以改变为它的基类,在泛型参数前填out
只有委托和接口中可以定义协变和逆变
委托
例如标准库中的Func委托
1 |
|
这段代码是合法的。
来看看Func<,>
的定义
1 |
|
说明第一个参数可以逆变,第二个参数可以协变。而ArgumentException派生自Exception,NullReferenceException派生自Exception,因此这段代码是合法的。
接口
与委托类似,IEnumerator<>
的定义是
1 |
|
因此IEnumerator<>
可以逆变
1 |
|
泛型约束
有时候,我们希望在泛型方法内调用传入形参的方法
1 |
|
噢,这段代码是无法通过编译的,因为我们根本不知道T类型有没有IsUnbreakable和IsGod属性!
但我们可以约束T的类型,使得它可以调用这些属性!
1 |
|
在上面的例子中,使用where关键字表示:指定泛型类型时,任何传入的类型都必须实现INormalProperty接口。这样,在调用方法时,如果传入参数未实现接口,则无法通过编译。
事实上,任何可以用泛型来定义的地方,都可以用where来约束。
泛型约束时,可以使用几个特殊的参数
- new():表示泛型可以支持无参构造
1 |
|
- class:表示泛型必须是引用类型
- struct:表示泛型必须是值类型
- unmanaged:表示泛型必须是值类型且不包含任何委托类型
其中class、struct、unmanaged是互斥的,不能同时出现,new()必须放在所有约束类型的末尾且与struct、unmanaged是互斥的。
还有几个特殊的类不能用于约束:
- System.Object
- System.Array
- System.Delegate
- System.MulticastDelegate
- System.ValueType
- System.Enum
- System.Void
一些小细节
将泛型变量设为默认值
我们有时候会写出这样的代码
1 |
|
这没问题,C#中的变量在使用前不可以只声明不初始化,但是泛型变量这样做可能会出一些问题。
1 |
|
这段代码无法通过编译,因为不知道它是值类型还是引用类型。我们可以用default关键字设置它的默认值,就像接口一样
1 |
|
泛型间的运算符
比较运算符
- 任何泛型变量都可以与null比较,如果泛型是值类型,
arg == null
永远都会返回false,因为值类型不可能是null - 两个引用类型的泛型变量可以直接用==或!=比较,结果和普通方法是一样的
- 两个未知类型的泛型变量不可以用操作符比较。可以约束接口
IEquatable<>
其他操作符
- 由于C#的限制,我们没有办法写出能处理任何数据类型的算法,任何操作符都不能用于泛型类型间的运算。C++可以通过重载运算符来成功编译模板函数,但C#尚不支持这样的做法。