简述
泛型向 .NET 引入了类型参数的概念。 泛型支持设计类和方法,你可在在代码中使用该类或方法时,再定义一个或多个类型参数的规范。 例如,通过使用泛型类型参数 T
,可以编写其他客户端代码能够使用的单个类,而不会产生运行时转换或装箱操作的成本或风险,如下所示:
// Declare the generic class. public class GenericList<T> { public void Add(T input) { } } class TestGenericList { private class ExampleClass { } static void Main() { // Declare a list of type int. GenericList<int> list1 = new GenericList<int>(); list1.Add(1); // Declare a list of type string. GenericList<string> list2 = new GenericList<string>(); list2.Add(""); // Declare a list of type ExampleClass. GenericList<ExampleClass> list3 = new GenericList<ExampleClass>(); list3.Add(new ExampleClass()); } }
泛型类和泛型方法兼具可重用性、类型安全性和效率,这是非泛型类和非泛型方法无法实现的。 在编译过程中将泛型类型参数替换为类型参数。 在前面的示例中,编译器会使用 int
替换 T
。 泛型通常与集合以及作用于集合的方法一起使用。 System.Collections.Generic 命名空间包含几个基于泛型的集合类。 不建议使用非泛型集合如 ArrayList,并且仅出于兼容性目的而维护非泛型集合。
你也可创建自定义泛型类型和泛型方法,以提供自己的通用解决方案,设计类型安全的高效模式。 以下代码示例演示了出于演示目的的简单泛型链接列表类。 (大多数情况下,应使用 .NET 提供的 List<T> 类,而不是自行创建类。)在通常使用具体类型来指示列表中所存储项的类型的情况下,可使用类型参数 T
:
- 在
AddHead
方法中作为方法参数的类型。 - 在
Node
嵌套类中作为Data
属性的返回类型。 - 在嵌套类中作为私有成员
data
的类型。
T
可用于 Node
嵌套类。 如果使用具体类型实例化 GenericList<T>
(例如,作为 GenericList<int>
),则出现的所有 T
都将替换为 int
。
// type parameter T in angle brackets public class GenericList<T> { // The nested class is also generic on T. private class Node { // T used in non-generic constructor. public Node(T t) { next = null; data = t; } private Node? next; public Node? Next { get { return next; } set { next = value; } } // T as private member data type. private T data; // T as return type of property. public T Data { get { return data; } set { data = value; } } } private Node? head; // constructor public GenericList() { head = null; } // T as method parameter type: public void AddHead(T t) { Node n = new Node(t); n.Next = head; head = n; } public IEnumerator<T> GetEnumerator() { Node? current = head; while (current != null) { yield return current.Data; current = current.Next; } } }
以下代码示例演示了客户端代码如何使用泛型 GenericList<T>
类来创建整数列表。 如果更改类型参数,以下代码将创建字符串列表或任何其他自定义类型:
class TestGenericList { static void Main() { // int is the type argument GenericList<int> list = new GenericList<int>(); for (int x = 0; x < 10; x++) { list.AddHead(x); } foreach (int i in list) { System.Console.Write(i + " "); } System.Console.WriteLine("\nDone"); } }
备注
泛型类型不限于类。 前面的示例使用了 class
类型,但你可以定义泛型 interface
和 struct
类型,包括 record
类型。
- 使用泛型类型可以最大限度地重用代码、保护类型安全性以及提高性能。
- 泛型最常见的用途是创建集合类。
- .NET 类库在 System.Collections.Generic 命名空间中包含几个新的泛型集合类。 应尽可能使用泛型集合来代替某些类,如 System.Collections命名空间中的 ArrayList
- 可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
- 可以对泛型类进行约束以访问特定数据类型的方法。
- 可以使用反射在运行时获取有关泛型数据类型中使用的类型的信息。
泛型类
泛型类封装不特定于特定数据类型的操作。 泛型类最常见用法是用于链接列表、哈希表、堆栈、队列和树等集合。 无论存储数据的类型如何,添加项和从集合删除项等操作的执行方式基本相同。
通常,创建泛型类是从现有具体类开始,然后每次逐个将类型更改为类型参数,直到泛化和可用性达到最佳平衡。 创建自己的泛型类时,需要考虑以下重要注意事项:
- 要将哪些类型泛化为类型参数。 通常,可参数化的类型越多,代码就越灵活、其可重用性就越高。 但过度泛化会造成其他开发人员难以阅读或理解代码。
- 要将何种约束(如有)应用到类型参数(请参阅类型参数的约束)。
其中一个有用的规则是,应用最大程度的约束,同时仍可处理必须处理的类型。 例如,如果知道泛型类仅用于引用类型,则请应用类约束。 这可防止将类意外用于值类型,并使你可在
T
上使用as
运算符和检查 null 值。 - 是否将泛型行为分解为基类和子类。 因为泛型类可用作基类,所以非泛型类的相同设计注意事项在此也适用。 请参阅本主题后文有关从泛型基类继承的规则。
- 实现一个泛型接口还是多个泛型接口。
例如,如果要设计用于在基于泛型的集合中创建项的类,则可能必须实现一个接口,例如 IComparable<T>,其中
T
为类的类型。
类型参数和约束的规则对于泛型类行为具有多种含义,尤其是在继承性和成员可访问性方面。 应当了解一些术语,然后再继续。 对于泛型类 Node<T>,
,客户端代码可通过指定类型参数来引用类,创建封闭式构造类型 (Node<int>
)。或者,可以不指定类型参数(例如指定泛型基类时),创建开放式构造类型 (Node<T>
)。 泛型类可继承自具体的封闭式构造或开放式构造基类:
class BaseNode { } class BaseNodeGeneric<T> { } // concrete type class NodeConcrete<T> : BaseNode { } //closed constructed type class NodeClosed<T> : BaseNodeGeneric<int> { } //open constructed type class NodeOpen<T> : BaseNodeGeneric<T> { }
非泛型类(即,具体类)可继承自封闭式构造基类,但不可继承自开放式构造类或类型参数,因为运行时客户端代码无法提供实例化基类所需的类型参数。
//No error class Node1 : BaseNodeGeneric<int> { } //Generates an error //class Node2 : BaseNodeGeneric<T> {} //Generates an error //class Node3 : T {}
继承自开放式构造类型的泛型类必须对非此继承类共享的任何基类类型参数提供类型参数,如下方代码所示:
class BaseNodeMultiple<T, U> { } //No error class Node4<T> : BaseNodeMultiple<T, int> { } //No error class Node5<T, U> : BaseNodeMultiple<T, U> { } //Generates an error //class Node6<T> : BaseNodeMultiple<T, U> {}
继承自开放式构造类型的泛型类必须指定作为基类型上约束超集或表示这些约束的约束:
class NodeItem<T> where T : System.IComparable<T>, new() { } class SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }
泛型类型可使用多个类型参数和约束,如下所示:
class SuperKeyType<K, V, U> where U : System.IComparable<U> where V : new() { }
开放式构造和封闭式构造类型可用作方法参数:
void Swap<T>(List<T> list1, List<T> list2) { //code to swap items } void Swap(List<int> list1, List<int> list2) { //code to swap items }
如果一个泛型类实现一个接口,则该类的所有实例均可强制转换为该接口。
泛型类是不变量。 换而言之,如果一个输入参数指定 List<BaseClass>
,且你尝试提供 List<DerivedClass>
,则会出现编译时错误。
泛型方法
泛型方法是通过类型参数声明的方法,如下所示:
static void Swap<T>(ref T lhs, ref T rhs) { T temp; temp = lhs; lhs = rhs; rhs = temp; }
如下示例演示使用类型参数的 int
调用方法的一种方式:
public static void TestSwap() { int a = 1; int b = 2; Swap<int>(ref a, ref b); System.Console.WriteLine(a + " " + b); }
还可省略类型参数,编译器将推断类型参数。 如下 Swap
调用等效于之前的调用:
Swap(ref a, ref b);
类型推理的相同规则适用于静态方法和实例方法。 编译器可基于传入的方法参数推断类型参数;而无法仅根据约束或返回值推断类型参数。 因此,类型推理不适用于不具有参数的方法。 类型推理发生在编译时,之后编译器尝试解析重载的方法签名。 编译器将类型推理逻辑应用于共用同一名称的所有泛型方法。 在重载解决方案步骤中,编译器仅包含在其上类型推理成功的泛型方法。
在泛型类中,非泛型方法可访问类级别类型参数,如下所示:
class SampleClass<T> { void Swap(ref T lhs, ref T rhs) { } }
如果定义一个具有与包含类相同的类型参数的泛型方法,则编译器会生成警告 CS0693,因为在该方法范围内,向内 T
提供的参数会隐藏向外 T
提供的参数。 如果需要使用类型参数(而不是类实例化时提供的参数)调用泛型类方法所具备的灵活性,请考虑为此方法的类型参数提供另一标识符,如下方示例中 GenericList2<T>
所示。
class GenericList<T> { // CS0693. void SampleMethod<T>() { } } class GenericList2<T> { // No warning. void SampleMethod<U>() { } }
使用约束在方法中的类型参数上实现更多专用操作。 此版 Swap<T>
现名为 SwapIfGreater<T>
,仅可用于实现 IComparable<T>的类型参数。
void SwapIfGreater<T>(ref T lhs, ref T rhs) where T : System.IComparable<T> { T temp; if (lhs.CompareTo(rhs) > 0) { temp = lhs; lhs = rhs; rhs = temp; } }
泛型方法可重载在数个泛型参数上。 例如,以下方法可全部位于同一类中:
void DoWork() { } void DoWork<T>() { } void DoWork<T, U>() { }
还可使用类型参数作为方法的返回类型。 下面的代码示例显示一个返回 T
类型数组的方法:
T[] Swap<T>(T a, T b) { return [b, a]; }