泛型(二)

風靈使發表於2019-01-12

型別引數的約束(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 約束不能與 structnew() 約束結合使用。

使用約束的原因

通過約束型別引數,可以增加約束型別及其繼承層次結構中的所有型別所支援的允許操作和方法呼叫的數量。 設計泛型類或方法時,如果要對泛型成員執行除簡單賦值之外的任何操作或呼叫 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 {/*...*/}
}

在上述示例中,TAdd 方法的上下文中是一個型別約束,而在 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.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;
//組合簽名確保組合委託必須具有相同的型別。
//var badCombined = first.TypeSafeCombine(test);

如果取消最後一行,它將不會編譯。 firsttest 均為委託型別,但它們是不同的委託型別。

列舉約束

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}");

相關文章