泛型,类型参数,泛型约束介绍

我被代码海扁 @codebeatme-s @103466775
阅读 5:32·字数 1664·发布 

泛型

泛型可用于推迟数据类型的确定,这项工作在正常的定义过程中往往需要立即进行。比如,在定义类时,你需要指定字段的数据类型,方法形参的数据类型等。

为何使用泛型?
很明显,泛型减少了设计层面的数据类型转换,同时保持了代码的适用性和简便性。

类型参数

泛型使用类型参数来代替本应被确定的数据类型,虽然也被称为参数,但类型参数的作用类似于占位符。在对使用了泛型的目标进行实例化或调用时,需要给出类型参数,比如 C# 中常见的泛型类List<T>,将其类型参数T替换为某种具体的数据类型后,实例化才能进行。

下面的 C# 代码实例化了泛型类List<T>

*.cs
// 类型参数 T 被指定为 string
List<string> nicknames = new();

泛型方法

如果你希望定义一个泛型方法,那么该方法需要拥有自己的类型参数,他不能与类或接口的类型参数相同,如果类或接口也运用了泛型的话。

下面的 C# 类Box拥有一个泛型方法GetFirst,用于寻找第一个数据类型与T一致的元素。

generics.cs
// 类 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寻找第一个字符串,整数,浮点数类型的元素。

*.cs
// 创建 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>()将产生错误。

generics.cs
public T? GetFirst<T>() where T : class
{
	// …
}

泛型的处理

对于接下来讲述的内容,我们作出如下假设,语言包含泛型类List<T>,以及引用类型object,值类型intfloat

在代码编译期间,泛型可能会以异构或同构的方式被处理,这取决于具体的语言编译器。

异构会为不同的类型参数构建不同的代码,如果你使用了List<int>List<float>,那么编译器需要为他们产生两个不同的类,以将类型参数T分别替换为intfloat

同构会将泛型的类型参数转换为最宽泛的数据类型,比如,无论你书写List<int>还是List<float>,编译器都会将类型参数T替换为object,因此,相对于异构,同构产生的类只有一个而不是多个。在这种情况下,编译器可能还需要调整某些代码,以完成额外的类型转换,毕竟,在开发人员书写的代码中,List<int>List<float>可以直接关联intfloat类型的值,这些值的类型与编译时出现的object类型存在转换的必要。

为何采用异构的泛型能够改善效率?
假如数据类型的转换将消耗大量资源,比如,引用类型与值类型之间的相互转换,那么采用异构的泛型会是一种很好的纾困方案,因为他使得相关的转换不再存在。

上述中的数据类型转换之所以会发生,是由于设计者希望采用一种宽泛的标准来涵盖所有的可能,比如,使用数组object[]来存储所有数据类型的值,当你将一个整数类型的值存入数组,或将其从数组中取出以进行算术运算时,引用类型object与值类型int之间的转换就会发生。而这样的情况会在使用异构泛型后消失,因为最终生成的数组是int[]而非object[],将int类型的值存入int[]当然不需要任何转换。

栈,堆
要想深入了解值类型和引用类型,你可以查看堆和引用一段。

源码

generics.cs·codebeatme/programming·GitHub