当“设计模式”出现时,人们提“用接口编程”;后来,有了泛型,人们提“用泛型编程”。什么泛型?比如,单链表 LinkedList 场景,每个节点包含两个字段:值和下一个节点的引用,其中,“值”既可以是 int,也可以是 string,为每个数据类型都写一个类,显然太麻烦,此时就可以使用泛型 LinkedList <T>;再如,排序算法中很常见 Swap 函数,交换两个值的位置,既可以是 int,也可以是 string,用泛型也很合适。用 T 代表 int 和 string,甚至任何类型。
但问题是,用 T 代表任何类型,显然太粗放,也不行。比如,要是用 T 表示动物和植物,动物和植物可能是接口或基类,显然动物和植物不同,顶多都是生物,我们倒是希望把 T 限定在动物或植物,这样在定义的泛型类中就可以使用动物或植物的成员——这就是泛型约束。
所以,T 实际项目中往往不是任何类型,而是代表某个类型,某个基类,某个接口,说是任何类型,只是泛型表达自己的思想。
如果真让 T 表示任何类型,显然不合实际,倒是把 T 限定在某个基类或接口倒是很不错的。
如果要检查泛型列表中的某个项以确定它是否有效,或者将它与其他某个项进行比较,则编译器必须在一定程度上保证它需要调用的运算符或方法将受到客户端代码可能指定的任何类型参数的支持。 这种保证是通过对泛型类定义应用一个或多个约束获得的。
例如,基类约束告诉编译器:仅此类型的对象或从此类型派生的对象才可用作类型参数。 一旦编译器有了这个保证,它就能够允许在泛型类中调用该类型的方法。约束是使用上下文关键字 where 应用的。
monospace; direction: ltr; border-top-style: none; color: black; font-size: 8pt; border-left-style: none; overflow: visible; padding-top: 0px">public class Employee
{
private string name;
private int id;
public Employee(string s, int i)
{
name = s;
id = i;
}
public string Name
{
get { return name; }
set { name = value; }
}
public int ID
{
get { return id; }
set { id = value; }
}
}
/// <summary>
/// 员工单链表
/// </summary>
/// <typeparam name="T"></typeparam>
public class EmployeeList<T> where T : Employee
{
/// <summary>
/// Employee 节点
/// </summary>
private class Node
{
private Node next;
private T data;
public Node(T t)
{
next = null;
data = t;
}
public Node Next
{
get { return next; }
set { next = value; }
}
public T Data
{
get { return data; }
set { data = value; }
}
}
private Node head;
public EmployeeList()
{
head = null;
}
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;
}
}
public T FindFirstOccurrence(string s)
{
Node current = head;
T t = null;
while (current != null)
{
//The constraint enables access to the Name property.
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
}
return t;
}
}
“where T : Employee”约束,使泛型类可以使用 Employee.Name 属性,类型为 T 的所有项,都保证是 Employee 对象或从 Employee 继承的对象。
泛型类的类型参数约束的作用非常有限,因为编译器除了假设类型参数派生自 System.Object 以外,不会做其他任何假设。 在希望强制两个类型参数之间的继承关系的情况下,可对泛型类使用参数类型约束。
在定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的类型种类施加限制。 如果客户端代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译时错误。 这些限制称为约束。 约束是使用 where 上下文关键字指定的。约束是使用 where 上下文关键字指定的。
下表列出了六种类型的约束:
约束
说明
T:结构
类型参数必须是值类型。
T:类
类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。
T:new()
类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。
T:<基类名>
类型参数必须是指定的基类或派生自指定的基类。
T:<接口名称>
类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。
T:U
为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。
可以对同一类型参数应用多个约束,并且约束自身可以是泛型类型,如下所示:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
// ...
}
通过约束类型参数,可以增加约束类型及其继承层次结构中的所有类型所支持的允许操作和方法调用的数量。 因此,在设计泛型类或方法时,如果要对泛型成员执行除简单赋值之外的任何操作或调用 System.Object 不支持的任何方法,您将需要对该类型参数应用约束。
在应用 where T : class 约束时,避免对类型参数使用 == 和 != 运算符,因为这些运算符仅测试引用同一性而不测试值相等性。 即使在用作参数的类型中重载这些运算符也是如此。 下面的代码说明了这一点;即使 String 类重载 == 运算符,输出也为 false。
public static void OpTest<T>(T s, T t) where T : class
{
System.Console.WriteLine(s == t);
}
static void Main()
{
string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = sb.ToString();
OpTest<string>(s1, s2);
}
这是因为,编译器在编译时仅知道 T 是引用类型,因此必须使用对所有引用类型都有效的默认运算符。这就好像对 int 类型和 string 类型的比较,显然不同。
如果必须测试值相等性,建议的方法是同时应用 where T : IComparable<T> 约束,并在将用于构造泛型类的任何类中实现该接口。
没有约束的类型参数(如公共类 SampleClass<T>{} 中的 T)称为未绑定的类型参数。 未绑定的类型参数具有以下规则:
将泛型类型参数作为约束使用,在具有自己类型参数的成员函数必须将该参数约束为包含类型的类型参数时非常有用,如下示例所示:
class List<T>
{
void Add<U>(List<U> items) where U : T {/*...*/}
}
在上面的示例中,T 在 Add 方法的上下文中是一个类型约束,而在 List 类的上下文中是一个未绑定的类型参数。
类型参数还可在泛型类定义中用作约束。 请注意,必须在尖括号中声明此类型参数与任何其他类型的参数:
//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }
下载 Demo