C#與CLR學習筆記(7)—— 泛型與泛型約束

1 泛型概述

1.1 含義

使用泛型的主要目的有三個:(1)實現代碼複用;(2)避免使用Object類,在實例化一個泛型類時,我們需要指定T的實際類型(類型實參),這樣保證了類型安全;(3)減少了 Object 造成的裝箱拆箱,提高性能(原理見下文)。
對於編譯器而言,泛型 T 本質上就是一個 類型參數(Type parameter),所謂參數其實就是一個特殊的佔位符。泛型被定義在程序集中,因此當代碼被編譯成 IL 放到程序集中時,T 仍然存在,例如以下案例:

Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
//以下是輸出
System.Collections.Generic.List`1[T]
System.Collections.Generic.Dictionary`2[TKey,TValue]

其中,後單引號(`)後面的數字代表類型參數的個數。
在代碼運行時,即在 JIT 階段,IL 被翻譯成 本機語言時,泛型類型參數 T 會被替換成具體的類型。例如,如果你使用了 List<int> 類,那麼代碼中的 T 是在 JIT 翻譯之後才變成了 int 類型。因此,不存在類型轉換、裝箱拆箱,這就是性能更好的原因。

需要注意,未指定類型實參的泛型類型是不能實例化的,因爲它是一種 開放類型,這與你不能實例化一個接口一樣。傳遞一個 類型實參 (Type argument)後,變成 封閉類型,纔可以實例化。案例:

Object o;
o = Activator.CreateInstance(typeof(List<int>));//OK
o = Activator.CreateInstance(typeof(List<>));//拋出ArgumentException,因爲含有泛型參數

泛型最常用的地方是集合類。微軟建議使用泛型集合,不建議使用非泛型集合,除了上文中提到的類型安全、性能高之外,泛型集合類中的虛方法更少,從而進一步提高執行性能;另外,泛型集合一般擁有更多的擴展方法,使用更方便。

1.2 泛型的繼承

1.2.1 泛型類型的繼承

泛型類可以派生自一個泛型基類,但是,泛型子類必須重複泛型基類的泛型類型,或者必須指定基類的泛型類型。
例如,考察以下代碼:

public class ChildGeneric : Generic<T>{} //編譯失敗
public class ChildGeneric<T> : Generic<T>{} //編譯通過
public class ChildGeneric : Generic<int>{} //編譯通過

1.2.2 泛型的類型參數的繼承

考察兩個泛型類型 List<MyBaseClass>List<MyChildClass>,其中 MyChildClass 派生自 MyBaseClass,那麼 List<MyBaseClass>List<MyChildClass> 之間有什麼關係嗎?答案是沒有關係。

因爲 類型參數 的繼承關係 不改變 泛型類型的 繼承關係,或者說,泛型類型 破壞了 泛型類型參數的繼承關係

更具體一點,指定類型實參並不影響層次結構。List<T> 派生自 Object,那麼 List<MyBaseClass>List<MyChildClass> 都是從 Object 派生,二者是 “平輩” 的。指定類型實參只是在 JIT 時拿指定的類型替換 T,這兩個 List 是兩個不同的類。

由此引出另一個話題,即 逆變協變,詳見我的另一篇文章《C#與CLR學習筆記(6)—— 輕鬆理解協變與逆變

2 泛型約束

2.1 編譯器對泛型參數的驗證

由於泛型參數 T 在定義時並未指定,因此,爲了安全性,編譯器會在編譯時進行分析,確保代碼適用於未來可能指定的任何泛型類型實參。
例如,考察如下代碼:

private static T Min<T>(T o1, T o2)
{
    if (o1.CompareTo(o2) < 0)
        return o1;
    return o2;
}

上述代碼編譯失敗,因爲並非所有類型都實現了 IComparable 接口,因此 CompareTo()方法有無法執行的風險。

所以從表面上看,使用泛型似乎做不了太多事情,只能使用 Object 中定義的方法。顯然,實際情況並不是這樣,因爲有 泛型約束 機制,它使得泛型變得有用。

上面的案例改進一下,就可以順利編譯:

private static T Min<T>(T o1, T o2) where T : IComparable<T>
{
    if (o1.CompareTo(o2) < 0)
        return o1;
    return o2;
}

約束 可應用於 泛型類型泛型方法 。如果 基類 或者 被重寫/實現的方法 擁有泛型約束,那麼子類或其方法必須應用同樣的泛型約束。
例如:

public class Generic<T> where T : struct
{}
public class ChildGeneric<T> : Generic<T> //編譯失敗
{}
public class ChildGeneric<T> : Generic<T> where T : struct //編譯通過
{}


public interface IGeneric2
{
     string GetTInfo<T>() where T : struct;
}
class ChildGeneric : IGeneric2
{
     public string GetTInfo<T>() where T : class //編譯失敗
     {
          return "";
     }
}
class ChildGeneric : IGeneric2
{
     public string GetTInfo<T>() where T : struct//編譯通過
     {
          return "";
     }
}

2.2 泛型約束的類型

分類 形式 含義
主要約束
(最多指定一個)
where T : struct T 必須是值類型
Nullable<T>除外,詳見參考文獻)
where T : class T 必須是引用類型
where T : Foo T 必須派生自 Foo 類
次要約束
(可以指定多個)
where T : IFoo T 必須實現 IFoo 接口
where T1 : T2 T1 派生自 泛型類型 T2
構造器約束 where T : new() T 是擁有公共無參構造函數的非抽象類

2.3 其他驗證問題

(1)類型轉換問題

泛型參數進行類型轉換,要保證符合約束。儘量使用 as 進行轉換。

public void DoSomething<T>(T obj)
{
    int x = (int)obj; //編譯錯誤
    int x = (int)(Object)obj; //編譯通過,但運行時可能拋 InvalidCastException
    stirng x = obj as string; //推薦
}

(2)將泛型類型轉爲默認值

推薦使用 default 關鍵字。

public void DoSomething<T>()
{
    T temp = null; //編譯錯誤,除非指定 T : class
    T temp = default(T); //推薦
}

(3)兩個泛型參數對比

需指定泛型約束。

public void DoSomething<T>(T o1, T o2)
{
    if (o1 == o2){} //編譯失敗,因爲若 T 是值類型,那麼可能沒有重載 == 運算符。
}

(4)不能將泛型約束爲具體的值類型,一是因爲值類型是隱式密封的,無法被繼承;二是因爲這種情況使用泛型就沒有意義。

參考文獻

[1] 《CLR via C#》 第四版

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章