泛型,类型参数,泛型约束介绍
泛型
泛型可用于推迟数据类型的确定,这项工作在正常的定义过程中往往需要立即进行。比如,在定义类时,你需要指定字段的数据类型,方法形参的数据类型等。
为何使用泛型?
很明显,泛型减少了设计层面的数据类型转换,同时保持了代码的适用性和简便性。
类型参数
泛型使用类型参数来代替本应被确定的数据类型,虽然也被称为参数,但类型参数的作用类似于占位符。在对使用了泛型的目标进行实例化或调用时,需要给出类型参数,比如 C# 中常见的泛型类List<T>
,将其类型参数T
替换为某种具体的数据类型后,实例化才能进行。
下面的 C# 代码实例化了泛型类List<T>
。
// 类型参数 T 被指定为 string
List<string> nicknames = new();
泛型方法
如果你希望定义一个泛型方法,那么该方法需要拥有自己的类型参数,他不能与类或接口的类型参数相同,如果类或接口也运用了泛型的话。
下面的 C# 类Box
拥有一个泛型方法GetFirst
,用于寻找第一个数据类型与T
一致的元素。
// 类 Box,表示存储内容的箱子
class Box
{
// 字段 items,一个简单的数组
object[] items;
// 构造器,可以初始化 items
public Box(params object[] i)
{
items = i ?? Array.Empty<object>();
}
// 泛型方法 GetFirst,获取数组中第一个类型为 T 的元素
public T? GetFirst<T>()
{
// 如果元素的类型与 T 一致,则返回
foreach (object item in items)
if (item.GetType() == typeof(T))
return (T)item;
return default;
}
}
使用泛型方法GetFirst
寻找第一个字符串,整数,浮点数类型的元素。
// 创建 Box 的实例 box
Box box = new("第一个字符串", 123, "第二个字符串", 234, 0.123f, 0.234f);
// 获取 box 中的第一个字符串
Console.WriteLine(box.GetFirst<string>());
// 获取 box 中的第一个整数
Console.WriteLine(box.GetFirst<int>());
// 获取 box 中的第一个浮点数
Console.WriteLine(box.GetFirst<float>());
第一个字符串
123
0.123
泛型约束
泛型约束主要用于限制泛型的使用,比如,限制类型参数可以指定的数据类型。对类型参数加以约束是理所当然的,因为他给予了开发人员很大的选择范围,没人愿意类型参数被指定为一种无法被处理的数据类型。
我们为GetFirst
方法的类型参数T
增加约束,使其只能被指定为引用类型,之前书写的box.GetFirst<int>()
和box.GetFirst<float>()
将产生错误。
public T? GetFirst<T>() where T : class
{
// …
}
泛型的处理
对于接下来讲述的内容,我们作出如下假设,语言包含泛型类List<T>
,以及引用类型object
,值类型int
,float
。
在代码编译期间,泛型可能会以异构或同构的方式被处理,这取决于具体的语言编译器。
异构会为不同的类型参数构建不同的代码,如果你使用了List<int>
和List<float>
,那么编译器需要为他们产生两个不同的类,以将类型参数T
分别替换为int
和float
。
同构会将泛型的类型参数转换为最宽泛的数据类型,比如,无论你书写List<int>
还是List<float>
,编译器都会将类型参数T
替换为object
,因此,相对于异构,同构产生的类只有一个而不是多个。在这种情况下,编译器可能还需要调整某些代码,以完成额外的类型转换,毕竟,在开发人员书写的代码中,List<int>
或List<float>
可以直接关联int
或float
类型的值,这些值的类型与编译时出现的object
类型存在转换的必要。
为何采用异构的泛型能够改善效率?
假如数据类型的转换将消耗大量资源,比如,引用类型与值类型之间的相互转换,那么采用异构的泛型会是一种很好的纾困方案,因为他使得相关的转换不再存在。
上述中的数据类型转换之所以会发生,是由于设计者希望采用一种宽泛的标准来涵盖所有的可能,比如,使用数组object[]
来存储所有数据类型的值,当你将一个整数类型的值存入数组,或将其从数组中取出以进行算术运算时,引用类型object
与值类型int
之间的转换就会发生。而这样的情况会在使用异构泛型后消失,因为最终生成的数组是int[]
而非object[]
,将int
类型的值存入int[]
当然不需要任何转换。
栈,堆
要想深入了解值类型和引用类型,你可以查看堆和引用一段。