泛型(二)
型別引數的約束(C# 程式設計指南)
約束告知編譯器型別引數必須具備的功能。 在沒有任何約束的情況下,型別引數可以是任何型別。 編譯器只能假定 Object
的成員,它是任何 .NET
型別的最終基類。 有關詳細資訊,請參閱使用約束的原因。 如果客戶端程式碼嘗試使用約束所不允許的型別來例項化類,則會產生編譯時錯誤。 通過使用 where
上下文關鍵字指定約束。 下表列出了七種型別的約束:
約束 | 說明 |
---|---|
where T : struct |
型別引數必須是值型別。 可以指定除 Nullable<T> 以外的任何值型別。 有關可以為 null 的型別的詳細資訊,請參閱可以為 null 的型別。 |
where T : class |
型別引數必須是引用型別。 此約束還應用於任何類、介面、委託或陣列型別。 |
where T : unmanaged |
型別引數不能是引用型別,並且任何巢狀級別均不能包含任何引用型別成員。 |
where T : new() |
型別引數必須具有公共無引數建構函式。 與其他約束一起使用時,new() 約束必須最後指定。 |
where T : <基類名> |
型別引數必須是指定的基類或派生自指定的基類。 |
where T : <介面名稱> |
型別引數必須是指定的介面或實現指定的介面。 可指定多個介面約束。 約束介面也可以是泛型。 |
where T : U |
為 T 提供的型別引數必須是為 U 提供的引數或派生自為 U 提供的引數。 |
某些約束是互斥的。 所有值型別必須具有可訪問的無引數建構函式。 struct
約束包含 new()
約束,且 new()
約束不能與 struct
約束結合使用。 unmanaged
約束包含 struct
約束。 unmanaged
約束不能與 struct
或 new()
約束結合使用。
使用約束的原因
通過約束型別引數,可以增加約束型別及其繼承層次結構中的所有型別所支援的允許操作和方法呼叫的數量。 設計泛型類或方法時,如果要對泛型成員執行除簡單賦值之外的任何操作或呼叫 System.Object
不支援的任何方法,則必須對該型別引數應用約束。 例如,基類約束告訴編譯器,僅此型別的物件或派生自此型別的物件可用作型別引數。 編譯器有了此保證後,就能夠允許在泛型類中呼叫該型別的方法。 以下程式碼示例演示可通過應用基類約束新增到(泛型介紹中的)GenericList<T>
類的功能。
public class Employee
{
public Employee(string s, int i) => (Name, ID) = (s, i);
public string Name { get; set; }
public int ID { get; set; }
}
public class GenericList<T> where T : Employee
{
private class Node
{
public Node(T t) => (Next, Data) = (null, t);
public Node Next { get; set; }
public T Data { get; set; }
}
private Node head;
public void AddHead(T t)
{
Node n = new Node(t) { 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)
{
//約束允許訪問Name屬性
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
}
return t;
}
}
約束使泛型類能夠使用 Employee.Name
屬性。 約束指定型別 T
的所有項都保證是 Employee
物件或從 Employee
繼承的物件。
可以對同一型別引數應用多個約束,並且約束自身可以是泛型型別,如下所示:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
// ...
}
在應用 where T : class
約束時,請避免對型別引數使用 ==
和 !=
運算子,因為這些運算子僅測試引用標識而不測試值相等性。 即使在用作引數的型別中過載這些運算子也會發生此行為。 下面的程式碼說明了這一點;即使 String
類過載 ==
運算子,輸出也為 false
。
public static void OpEqualsTest<T>(T s, T t) where T : class
{
System.Console.WriteLine(s == t);
}
private static void TestStringEquality()
{
string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = sb.ToString();
OpEqualsTest<string>(s1, s2);
}
編譯器只知道 T
在編譯時是引用型別,並且必須使用對所有引用型別都有效的預設運算子。 如果必須測試值相等性,建議同時應用 where T : IEquatable<T>
或 where T : IComparable<T>
約束,並在用於構造泛型類的任何類中實現該介面。
約束多個引數
可以對多個引數應用多個約束,對一個引數應用多個約束,如下例所示:
class Base { }
class Test<T, U> where U : struct
where T : Base, new()
{ }
未繫結的型別引數
沒有約束的型別引數(如公共類 SampleClass<T>{}
中的 T
)稱為未繫結的型別引數。 未繫結的型別引數具有以下規則:
- 不能使用
!=
和==
運算子,因為無法保證具體的型別引數能支援這些運算子。 - 可以在它們與
System.Object
之間來回轉換,或將它們顯式轉換為任何介面型別。 - 可以將它們與
null
進行比較。 將未繫結的引數與null
進行比較時,如果型別引數為值型別,則該比較將始終返回false
。
型別引數作為約束
在具有自己型別引數的成員函式必須將該引數約束為包含型別的型別引數時,將泛型型別引數用作約束非常有用,如下例所示:
public class List<T>
{
public void Add<U>(List<U> items) where U : T {/*...*/}
}
在上述示例中,T
在 Add
方法的上下文中是一個型別約束,而在 List
類的上下文中是一個未繫結的型別引數。
型別引數還可在泛型類定義中用作約束。 必須在尖括號中宣告該型別引數以及任何其他型別引數:
//型別引數V用作型別約束
public class SampleClass<T, U, V> where T : V { }
型別引數作為泛型類的約束的作用非常有限,因為編譯器除了假設型別引數派生自 System.Object
以外,不會做其他任何假設。 如果要在兩個型別引數之間強制繼承關係,可以將型別引數用作泛型類的約束。
非託管約束
從 C# 7.3
開始,可使用 unmanaged
約束來指定型別引數必須為“非託管型別”。 “非託管型別”不是引用型別,且任何巢狀級別都不包含引用型別欄位。 通過 unmanaged
約束,使用者能編寫可重用例程,從而使用可作為記憶體塊操作的型別,如以下示例所示:
unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{
var size = sizeof(T);
var result = new Byte[size];
Byte* p = (byte*)&argument;
for (var i = 0; i < size; i++)
result[i] = *p++;
return result;
}
以上方法必須在 unsafe
上下文中編譯,因為它並不是在已知的內建型別上使用 sizeof
運算子。 如果沒有 unmanaged
約束,則 sizeof
運算子不可用。
委託約束
同樣從 C# 7.3
開始,可將 System.Delegate
或 System.MulticastDelegate
用作基類約束。 CLR
始終允許此約束,但 C#
語言不允許。 使用 System.Delegate
約束,使用者能夠以型別安全的方式編寫使用委託的程式碼。 以下程式碼定義了合併兩個同型別委託的擴充套件方法:
public static TDelegate TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
where TDelegate : System.Delegate
=> Delegate.Combine(source, target) as TDelegate;
可使用上述方法來合併相同型別的委託:
Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");
var combined = first.TypeSafeCombine(second);
combined();
Func<bool> test = () => true;
//組合簽名確保組合委託必須具有相同的型別。
//var badCombined = first.TypeSafeCombine(test);
如果取消最後一行,它將不會編譯。 first
和 test
均為委託型別,但它們是不同的委託型別。
列舉約束
從 C# 7.3
開始,還可指定 System.Enum
型別作為基類約束。 CLR
始終允許此約束,但 C#
語言不允許。 使用 System.Enum
的泛型提供型別安全的程式設計,快取使用 System.Enum
中靜態方法的結果。 以下示例查詢列舉型別的所有有效的值,然後生成將這些值對映到其字串表示形式的字典。
public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
var result = new Dictionary<int, string>();
var values = Enum.GetValues(typeof(T));
foreach (int item in values)
result.Add(item, Enum.GetName(typeof(T), item));
return result;
}
所用方法利用反射,這會對效能產生影響。 可呼叫此方法來生成可快取和重用的集合,而不是重複需要反射才能實施的呼叫。
如以下示例所示,可使用它來建立列舉並生成其值和名稱的字典:
enum Rainbow
{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}
var map = EnumNamedValues<Rainbow>();
foreach (var pair in map)
Console.WriteLine($"{pair.Key}:\t{pair.Value}");
相關文章
- 詳解C#泛型(二)C#泛型
- Java泛型知識點:泛型類、泛型介面和泛型方法Java泛型
- 泛型類、泛型方法及泛型應用泛型
- 泛型協變與抗變(二)泛型
- 泛型類和泛型方法泛型
- 【java】【泛型】泛型geneticJava泛型
- java泛型之泛型方法。Java泛型
- 泛型--泛型萬用字元和泛型的上下限泛型字元
- TypeScript 泛型介面和泛型類TypeScript泛型
- Go 泛型之泛型約束Go泛型
- java泛型之泛型陣列。Java泛型陣列
- 泛型泛型
- 泛型最佳實踐:Go泛型設計者教你如何用泛型泛型Go
- 型別 VS 泛型型別泛型
- TypeScript 泛型型別TypeScript泛型型別
- 泛型(一)泛型
- 泛型(三)泛型
- 泛型(四)泛型
- 泛型(五)泛型
- Go 泛型Go泛型
- va泛型泛型
- Java 泛型Java泛型
- 泛型viewmodle泛型View
- Java泛型Java泛型
- 泛型類、泛型方法、型別萬用字元的使用泛型型別字元
- 【譯】在非泛型類中建立泛型方法泛型
- 泛型型別(.NET 指南)泛型型別
- Go 官方出品泛型教程:如何開始使用泛型Go泛型
- Java函式泛型List引數,操作泛型元素Java函式泛型
- JAVA泛型使用練習2(自定義泛型使用)Java泛型
- 泛型類及系統中常用的泛型類泛型
- Java 泛型原理Java泛型
- 介面即泛型泛型
- TypeScript 泛型相容TypeScript泛型
- TypeScript 泛型限定TypeScript泛型
- Java+泛型Java泛型
- Golang 泛型初探Golang泛型
- 關於泛型泛型