當“設計模式”出現時,人們提“用接口編程”;后來,有了泛型,人們提“用泛型編程”。什么泛型?比如,單鏈表 LinkedList 場景,每個節點包含兩個字段:值和下一個節點的引用,其中,“值”既可以是 int,也可以是 string,甚至是對象,為每個數據類型都寫一個類,顯然太麻煩,此時就可以使用泛型 LinkedList <T>,T 表示 int 或 string 類型等等;再如,排序算法中很常見 Swap(ref int a, ref int b) 函數,可以交換兩個 int 類型,當然也可以是 string,用泛型也很合適。用 T 代表 int 和 string,甚至任何類型。
但問題是,實際項目中用 T 表示任何類型,顯然太粗放。比如,要是用 T 表示動物和植物,動物和植物可能是接口或基類,顯然動物和植物不同,頂多都繼承生物基類或接口,我們倒是希望把 T 限定在動物或植物,這樣在定義相應的泛型類中就可以使用動物或植物的成員——這就是泛型約束。
這就完美了~
所以,實際項目中T 往往不是任何類型,而是代表某個類型、某個基類、某個接口,說是任何類型,只是泛型表達自己的理念而已。
如果把 T 限定在某個基類、某個接口上,那么泛型類中就可以使用那個基類或接口中的成員。
如果要檢查泛型列表中的某個項以確定它是否有效,或者將它與其他某個項進行比較,則編譯器必須在一定程度上保證它需要調用的運算符或方法將受到客戶端代碼可能指定的任何類型參數的支持。 這種保證是通過對泛型類定義應用一個或多個約束獲得的。
例如,基類約束告訴編譯器:僅此類型的對象或從此類型派生的對象才可用作類型參數。 一旦編譯器有了這個保證,它就能夠允許在泛型類中調用該類型的方法。約束是使用關鍵字 where 。
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 屬性,即 current.Data.Name。
類型為 T 的所有項,都保證是 Employee 對象或從 Employee 繼承的對象。
編譯器除了假設類型參數派生自 System.Object 以外,不會做其他任何假設。在希望強制兩個類型參數之間的繼承關系的情況下,可對泛型類使用參數類型約束。
在定義泛型類時,可以對客戶端代碼能夠在實例化類時用于類型參數的類型種類施加限制。 如果客戶端代碼嘗試使用某個約束所不允許的類型來實例化類,則會產生編譯時錯誤。 這些限制稱為約束。 約束是使用 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 方法中有一個類型約束 where U : T,其中,Add 方法中使用了泛型 U,而在 List 類中并沒有綁定的類型參數,沒有約束。
類型參數還可在泛型類定義中用作約束。注意,必須在尖括號中聲明此類型參數與任何其他類型的參數:
//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }
下載 Demo
新聞熱點
疑難解答