共用方式為


型別參數的條件約束 (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 條件約束,不能與 structnew() 條件約束結合。
where T : new() 型別引數必須有公用無參數建構函式。 與其他條件約束搭配使用時,new() 條件約束必須是最後一個指定的。 new() 條件約束不能與 structunmanaged 條件約束合併使用。
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 條件約束表示不含 classstruct 條件約束的基底方法。 如需詳細資訊,請參閱 default 條件約束規格提案。

某些條件約束是互斥的,而某些條件約束必須依指定順序排列:

  • 您最多可以套用其中一個 structclassclass?notnullunmanaged 條件約束。 如果您提供上述任一條件約束,則其必須是針對該型別參數指定的第一個條件約束。
  • 基底類別條件約束 (where T : Basewhere T : Base?) 無法與任何條件約束 structclassclass?notnullunmanaged 結合。
  • 您可以使用任一形式,最多套用一個基底類別條件約束。 如果您要支援可為 Null 的基底類型,請使用 Base?
  • 您無法同時將介面的不可為 Null 和可為 Null 的形式都命名為條件約束。
  • new() 條件約束不能與 structunmanaged 條件約束合併使用。 如果您指定 new() 條件約束,其必須是該型別參數的最後一個條件約束。
  • 只能在覆寫或明確介面實作上套用 default 條件約束。 其無法與 structclass 條件約束結合。

為什麼使用條件約束

條件約束會指定型別參數的功能和期望。 宣告這些條件約束表示您可以使用限制型別的作業和方法呼叫。 當泛型類別或方法對簡單指派 (包含呼叫 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 : Employee, System.Collections.Generic.IList<T>, IDisposable, new()
{
    // ...
}

套用 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 {/*...*/}
}

在上述範例中,TAdd 方法內容中的類型條件約束,以及 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? 可以搭配 structclass 條件約束使用,但其中一個必須存在。 使用 class 條件約束時,T? 參考 的可為 Null 的 T 參考型別。 當兩個條件約束都未套用時,可以使用 T?。 在此情況下,T? 會解譯為實值型別和參考型別的 T?。 不過,如果 TNullable<T> 的執行個體,則 T?T 相同。 換句話說,這不會變成 T??

因為 T? 現在可以在沒有 classstruct 條件約束的情況下使用,所以覆寫或明確介面實作中可能會發生模棱兩可的情況。 在這兩種情況下,覆寫不會包含條件約束,而是繼承自基底類別。 基底類別不套用 classstruct 條件約束時,衍生類別必須以某種方式指定套用於基底方法的覆寫,完全不需要條件約束。 此衍生方法運用 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.DelegateSystem.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);

如果您將最後一行取消註解,則不會編譯它。 firsttest 都是委派型別,但這些是不同的委派型別。

列舉條件約束

您也可以指定 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.GetValuesEnum.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 virtualstatic 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);
}

上述語法需要實作者針對這些方法使用明確的介面實作。 提供額外的條件約束可讓介面根據型別參數定義運算子。 實作介面的型別可以隱含地實作介面方法。

另請參閱