型別參數的條件約束 (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 類型。 條件 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約束表示不含 或 struct 條件約束的class基底方法。 如需詳細資訊,請參閱 default 條件約束 規格提案。

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

  • 您最多可以套用其中struct一個 、class、、 notnullclass?unmanaged 條件約束。 如果您提供上述任一條件約束,它必須是針對該類型參數指定的第一個條件約束。
  • 基類條件約束 (where T : Basewhere T : Base?) 無法與任何條件約束 structclass、、 class?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 條件約束,編譯程式會產生警告,而不是錯誤。

條件 notnull 約束只有在可為 Null 的內容中使用時才有作用。 如果您在可為 Null 的可遺忘內容中新增 notnull 條件約束,編譯程式不會針對違反條件約束而產生任何警告或錯誤。

class 約束

class可為 Null 內容中的條件約束會指定類型自變數必須是不可為 Null 的參考型別。 在可為 Null 的內容中,當類型自變數為可為 Null 的參考型別時,編譯程式會產生警告。

default 約束

新增可為 Null 的參考型別會使泛型型別或方法中的 用法 T? 複雜。 T? 可以搭配 structclass 條件約束使用,但其中一個必須存在。 class使用條件約束時,T?會參考的可為 Null 參考型別TT? 當兩個條件約束都未套用時,即可使用。 在此情況下, T? 會解譯為 T? 實值型別和參考型別。 不過,如果 T 是的 Nullable<T>實例, T? 則與 T相同。 換句話說,它不會變成 T??

因為T?現在可以在沒有 或 struct 條件約束的情況下class使用,因此覆寫或明確介面實作中可能會出現模棱兩可的情況。 在這兩種情況下,覆寫不包含條件約束,而是繼承自基類。 當基類未套用 classstruct 條件約束時,衍生類別必須以某種方式指定套用至基底方法的覆寫,而不需要條件約束。 衍生方法會 default 套用條件約束。 條件default約束不會class釐清和 struct 條件約束。

非受控條件約束

您可以使用 unmanaged 條件約束來指定類型參數必須是不可為 Null 的 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 運算子無法使用。

條件 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);

如果您將最後一行取消註解,則不會編譯它。 和 test 都是first委派類型,但它們是不同的委派類型。

列舉條件約束

您也可以將 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>
{
    public abstract static T operator +(T left, T right);
    public abstract static T operator -(T left, T right);
}

此模式可讓 C# 編譯程式判斷多載運算子或任何 static virtualstatic abstract 方法的包含類型。 它會提供語法,以便在包含型別上定義加法和減法運算元。 如果沒有這個條件約束,參數和自變數必須宣告為介面,而不是類型參數:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    public abstract static IAdditionSubtraction<T> operator +(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);

    public abstract static IAdditionSubtraction<T> operator -(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);
}

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

另請參閱