在定義泛型類時,可以對用戶端代碼能夠在執行個體化類時用於型別參數的類型種類施加限制。如果用戶端代碼嘗試使用某個約束所不允許的類型來執行個體化類,則會產生編譯時間錯誤。這些限制稱為約束。約束是使用 where 內容關鍵字指定的。下表列出了六種類型的約束:
| 約束 |
說明 |
T:結構 |
型別參數必須是實值型別。可以指定除 Nullable 以外的任何實值型別。有關更多資訊,請參見使用可空類型(C# 編程指南)。 |
T:類 |
型別參數必須是參考型別,包括任何類、介面、委託或數群組類型。 |
T:new() |
型別參數必須具有無參數的公用建構函式。當與其他約束一起使用時,new() 約束必須最後指定。 |
T:<基類名> |
型別參數必須是指定的基類或派生自指定的基類。 |
T:<介面名稱> |
型別參數必須是指定的介面或實現指定的介面。可以指定多個介面約束。約束介面也可以是泛型的。 |
T:U |
為 T 提供的型別參數必須是為 U 提供的參數或派生自為 U 提供的參數。這稱為裸類型約束。 |
使用約束的原因
如果要檢查泛型列表中的某個項以確定它是否有效,或者將它與其他某個項進行比較,則編譯器必須在一定程度上保證它需要調用的運算子或方法將受到用戶端代碼可能指定的任何型別參數的支援。這種保證是通過對泛型類定義應用一個或多個約束獲得的。例如,基類約束告訴編譯器:僅此類型的對象或從此類型派生的對象才可用作型別參數。一旦編譯器有了這個保證,它就能夠允許在泛型類中調用該類型的方法。約束是使用內容關鍵字 where 應用的。下面的程式碼範例示範可通過應用基類約束添加到 GenericList<T> 類(在泛型介紹(C# 編程指南)中)的功能。
C#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; }
}
}
public class GenericList<T> where T : Employee
{
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 GenericList() //constructor
{
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;
}
}
約束使得泛型類能夠使用 Employee.Name 屬性,因為類型為 T 的所有項都保證是 Employee 對象或從 Employee 繼承的對象。
可以對同一型別參數應用多個約束,並且約束自身可以是泛型型別,如下所示:
C#
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new(){// ...}
通過約束型別參數,可以增加約束類型及其繼承階層中的所有類型所支援的允許操作和方法調用的數量。因此,在設計泛型類或方法時,如果要對泛型成員執行除簡單賦值之外的任何操作或調用 System.Object 不支援的任何方法,您將需要對該型別參數應用約束。
在應用 where T : class 約束時,建議不要對型別參數使用 == 和 != 運算子,因為這些運算子僅測試引用同一性而不測試值相等性。即使在用作參數的類型中重載這些運算子也是如此。下面的代碼說明了這一點;即使 String 類重載 == 運算子,輸出也為 false。
C#
public static void OpTest<T>(T s, T t) where T : class{System.Console.WriteLine(s == t);}static void Main(){string s1 = "foo";System.Text.StringBuilder sb = new System.Text.StringBuilder("foo");string s2 = sb.ToString();OpTest<string>(s1, s2);}
這種情況的原因在於,編譯器在編譯時間僅知道 T 是參考型別,因此必須使用對所有參考型別都有效預設運算子。如果需要測試值相等性,建議的方法是同時應用 where T : IComparable<T> 約束,並在將用於構造泛型類的任何類中實現該介面。
未綁定的型別參數
沒有約束的型別參數(如公用類 SampleClass<T>{} 中的 T)稱為未綁定的型別參數。未綁定的型別參數具有以下規則:
不能使用 != 和 == 運算子,因為無法保證具體型別參數能支援這些運算子。
可以在它們與 System.Object 之間來迴轉換,或將它們顯式轉換為任何介面類型。
可以將它們與 null 進行比較。將未綁定的參數與 null 進行比較時,如果型別參數為實值型別,則該比較將始終返回 false。
裸類型約束
用作約束的泛型型別參數稱為裸類型約束。當具有自己的型別參數的成員函數需要將該參數約束為包含類型的型別參數時,裸類型約束很有用,如下面的樣本所示:
C#
class List<T>{void Add<U>(List<U> items) where U : T {/*...*/}}
在上面的樣本中,T 在 Add 方法的上下文中是一個裸類型約束,而在 List 類的上下文中是一個未綁定的型別參數。
裸類型約束還可以在泛型類定義中使用。注意,還必須已經和其他任何型別參數一起在角括弧中聲明了裸類型約束:
C# 複製代碼
//naked type constraintpublic class SampleClass<T, U, V> where T : V { }
泛型類的裸類型約束的作用非常有限,因為編譯器除了假設某個裸類型約束派生自 System.Object 以外,不會做其他任何假設。在希望強制兩個型別參數之間的繼承關係的情況下,可對泛型類使用裸類型約束。