型別參數的條件約束 (C# 程式設計手冊)
條件約束會通知編譯器有關型別引數必須要有的功能。 如果沒有任何條件約束,則型別引數可以是任何型別。 編譯器只能採用 System.Object 成員,這是任何 .NET 型別的最終基底類別。 如需詳細資訊,請參閱為什麼使用條件約束。 如果用戶端程式碼使用不符合條件約束的型別,編譯器就會發出錯誤。 條件約束是使用 where
內容關鍵字所指定。 下表列出各種型別的條件約束:
條件約束 | 描述 |
---|---|
where T : struct |
型別引數必須是不可為 Null 的實值型別,其中包含 record struct 型別。 如需可為 Null 值型別的資訊,請參閱可為 Null 的值型別。 所有實值型別都有可存取的無參數建構函式 (無論是宣告或隱含),因此 struct 條件約束表示 new() 條件約束,而且無法與 new() 條件約束結合。 您無法將 struct 條件約束與 unmanaged 條件約束結合。 |
where T : class |
型別引數必須是參考型別。 此條件約束也適用於任何類別、介面、委派或陣列型別。 在可為 Null 的內容中,T 必須是不可為 Null 的參考型別。 |
where T : class? |
型別引數必須是可為 Null 或不可為 Null 的參考型別。 此條件約束也適用於任何類別、介面、委派或陣列型別,包含記錄。 |
where T : notnull |
型別引數必須是不可為 Null 的型別。 引數可以是不可為 Null 的參考型別或不可為 Null 的實值型別。 |
where T : unmanaged |
型別引數必須是不可為 Null 的非受控型別。 unmanaged 條件約束表示 struct 條件約束,不能與 struct 或 new() 條件約束結合。 |
where T : new() |
型別引數必須有公用無參數建構函式。 與其他條件約束搭配使用時,new() 條件約束必須是最後一個指定的。 new() 條件約束不能與 struct 和 unmanaged 條件約束合併使用。 |
where T : <基底類別名稱> |
型別引數必須是或衍生自指定的基底類別。 在可為 Null 的內容中,T 必須是衍生自指定基底類別的非可為 Null 參考型別。 |
where T : <基底類別名稱>? |
型別引數必須是或衍生自指定的基底類別。 在可為 Null 的內容中,T 必須是衍生自指定基底類別的可為 Null 或不可為 Null 型別。 |
where T : <介面名稱> |
型別引數必須是或實作指定的介面。 您可以指定多個介面條件約束。 條件約束介面也是泛型。 在可為 Null 的內容中,T 必須是實作指定介面的非可為 Null 型別。 |
where T : <介面名稱>? |
型別引數必須是或實作指定的介面。 您可以指定多個介面條件約束。 條件約束介面也是泛型。 在可為 Null 的內容中,T 可以是可為 Null 的參考型別、不可為 Null 的參考型別或實值型別。 T 不能是可為 Null 的實值型別。 |
where T : U |
針對 T 提供的型別引數必須是或衍生自針對 U 所提供的引數。 在可為 Null 的內容中,如果 U 是不可為 Null 的參考型別,T 則必須是不可為 Null 的參考型別。 如果 U 是可為 Null 的參考型別,T 可能是可為 Null 或不可為 Null。 |
where T : default |
您覆寫方法或提供明確的介面實作時,此條件約束會解決需要指定未限制型別參數時的模棱兩可。 default 條件約束表示不含 class 或 struct 條件約束的基底方法。 如需詳細資訊,請參閱 default 條件約束規格提案。 |
where T : allows ref struct |
此反條件約束會宣告 T 的型別引數可以是 ref struct 型別。 泛型型別或方法必須遵守 T 的任何實例的參考安全規則,因為它可能是 ref struct 。 |
某些條件約束是互斥的,而某些條件約束必須依指定順序排列:
- 您最多可以套用其中一個
struct
、class
、class?
、notnull
和unmanaged
條件約束。 如果您提供上述任一條件約束,則其必須是針對該型別參數指定的第一個條件約束。 - 基底類別條件約束 (
where T : Base
或where T : Base?
) 不能與任何條件約束struct
、class
、class?
、notnull
或unmanaged
結合使用。 - 您可以使用任一形式,最多套用一個基底類別條件約束。 如果您要支援可為 Null 的基底類型,請使用
Base?
。 - 您無法同時將介面的不可為 Null 和可為 Null 的形式都命名為條件約束。
new()
條件約束不能與struct
或unmanaged
條件約束合併使用。 如果您指定new()
條件約束,其必須是該型別參數的最後一個條件約束。 如果適用,反條件約束可以遵循new()
條件約束。- 只能在覆寫或明確介面實作上套用
default
條件約束。 其無法與struct
或class
條件約束結合。 allows ref struct
反條件約束不能與class
或class?
條件約束結合使用。allows ref struct
反條件約束必須遵循該型別參數的所有條件約束。
為什麼使用條件約束
條件約束會指定型別參數的功能和期望。 宣告這些條件約束表示您可以使用限制型別的作業和方法呼叫。 當泛型類別或方法對簡單指派 (包含呼叫 System.Object 不支援的任何方法) 以外的泛型成員使用任何作業時,請將條件約束套用至型別參數。 例如,基底類別條件約束會告知編譯器只有這個類型的物件或衍生自這個類型的物件才會取代該型別引數。 編譯器具有這項保證之後,就可以允許在泛型類別中呼叫該類型的方法。 下列程式碼範例示範您可以套用基底類別條件約束來新增至 GenericList<T>
類別的功能 (在泛型簡介中)。
public class Employee
{
public Employee(string name, int id) => (Name, ID) = (name, id);
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)
{
//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
的物件。
多個條件約束可以套用至相同的型別參數,而且條件約束本身可以是泛型型別,如下所示:
class EmployeeList<T> where T : notnull, Employee, IComparable<T>, new()
{
// ...
public void AddDefault()
{
T t = new T();
// ...
}
}
套用 where T : class
條件約束時,請避免在型別參數上使用 ==
和 !=
運算子,因為這些運算子只會測試參考識別是否相等,但不會測試值是否相等。 即使在用作引數的型別中多載這些運算子,也會發生這種行為。 下列程式碼說明這點;輸出為 false,即使 String 類別多載 ==
運算子也是一樣。
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
類別內容中的未繫結型別參數。
型別參數也可以在泛型類別定義中用作條件約束。 型別參數必須與任何其他型別參數一起宣告於角括弧內:
//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }
型別參數作為條件約束對泛型類別來說不實用,因為編譯器除了會假設型別參數衍生自 System.Object
之外,不會再做其他任何假設。 如果您要強制兩個型別參數之間具有繼承關係,請在泛型類別上將型別參數用作條件約束。
notnull
條件約束
您可以使用 notnull
條件約束來指定型別引數必須是不可為 Null 的實值型別或不可為 Null 的參考型別。 與其他大部分條件約束不同,如果型別引數違反 notnull
條件約束,編譯器會產生警告,而不是錯誤。
只有在可為 Null 的內容中使用時,notnull
條件約束才會生效。 如果您在可為 Null 的模糊內容中新增 notnull
條件約束,編譯器不會針對違反條件約束而產生任何警告或錯誤。
class
條件約束
可為 Null 內容中的 class
條件約束會指定型別引數必須是不可為 Null 的參考型別。 在可為 Null 的內容中,型別引數為可為 Null 的參考型別時,編譯器會產生警告。
default
條件約束
新增可為 Null 的參考型別會使泛型型別或方法中的 T?
用法複雜。 T?
可以搭配 struct
或 class
條件約束使用,但其中一個必須存在。 使用 class
條件約束時,T?
參考 的可為 Null 的 T
參考型別。 當兩個條件約束都未套用時,可以使用 T?
。 在此情況下,T?
會解譯為實值型別和參考型別的 T?
。 不過,如果 T
是 Nullable<T> 的執行個體,則 T?
與 T
相同。 換句話說,這不會變成 T??
。
因為 T?
現在可以在沒有 class
或 struct
條件約束的情況下使用,所以覆寫或明確介面實作中可能會發生模棱兩可的情況。 在這兩種情況下,覆寫不會包含條件約束,而是繼承自基底類別。 基底類別不套用 class
或 struct
條件約束時,衍生類別必須以某種方式指定套用於基底方法的覆寫,完全不需要條件約束。 此衍生方法運用 default
條件約束。 條件 default
約束 不會class
厘清 和 struct
條件約束。
非受控條件約束
您可以使用 unmanaged
條件約束指定型別參數必須是不可為 Null 的非受控型別。 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
運算子無法使用。
unmanaged
條件約束表示 struct
條件約束,不能合併使用。 因為 struct
條件約束表示 new()
條件約束,所以 unmanaged
條件約束不能與 new()
條件約束合併使用。
委派條件約束
您可以使用 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;
// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);
如果您取消最後一行的註解,則它不會進行編譯。 first
和 test
都是委派型別,但這些是不同的委派型別。
列舉條件約束
您也可以指定 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.GetValues
和 Enum.GetName
會使用反映,而這會影響效能。 您可以呼叫 EnumNamedValues
建置可快取和重複使用的集合,而不是重複需要反映的呼叫。
您可以如下列範例所示使用它來建立列舉,並建置其值和名稱的字典:
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}");
型別引數會實作宣告的介面
某些案例需要為型別參數提供的引數會實作該介面。 例如:
public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
static abstract T operator +(T left, T right);
static abstract T operator -(T left, T right);
}
此模式可讓 C# 編譯器判斷多載運算子或任何 static virtual
或 static abstract
方法的包含型別。 這提供語法,讓加法和減法運算子可以在包含的型別上定義。 如果沒有此條件約束,就必須將參數和引數宣告為介面,而不是型別參數:
public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
static abstract IAdditionSubtraction<T> operator +(
IAdditionSubtraction<T> left,
IAdditionSubtraction<T> right);
static abstract IAdditionSubtraction<T> operator -(
IAdditionSubtraction<T> left,
IAdditionSubtraction<T> right);
}
上述語法需要實作者針對這些方法使用明確的介面實作。 提供額外的條件約束可讓介面根據型別參數定義運算子。 實作介面的型別可以隱含地實作介面方法。
允許 ref struct
allows ref struct
反條件約束宣告對應的型別引數可以是 ref struct
型別。 該型別參數的實例必須遵守下列規則:
- 它無法進行 Boxed。
- 它參與了參考安全規則。
- 不能在不允許使用
ref struct
型別的位置使用實例,例如static
欄位。 - 實例可以使用
scoped
修飾元來標示。
allows ref struct
子句不會被繼承。 在下列程式碼中:
class SomeClass<T, S>
where T : allows ref struct
where S : T
{
// etc
}
S
的引數不能是 ref struct
,因為 S
沒有 allows ref struct
子句。
具有 allows ref struct
子句的型別參數不能用作型別引數,除非對應的型別參數也具有 allows ref struct
子句。 以下範例示範了此規則:
public class Allow<T> where T : allows ref struct
{
}
public class Disallow<T>
{
}
public class Example<T> where T : allows ref struct
{
private Allow<T> fieldOne; // Allowed. T is allowed to be a ref struct
private Disallow<T> fieldTwo; // Error. T is not allowed to be a ref struct
}
上面的範例顯示,可能是 ref struct
型別的型別引數不能取代不能是 ref struct
型別的型別參數。