我們在開發時往往會對泛型指定限制條件,隻有類型參數符合條件的才允許用在這個泛型上面。但是有時我們會定義過多或過少的限制條件,過多的限制條件會導緻其他開發人員在使用你所編寫的方法或類時做很多的工作以滿足這些限制,過少的限制又會導緻程式在運作的時候必須做很多的檢查,并執行更多的強制類型轉化操作,有時我們還需要使用反射生成運作期錯誤,來防止使用者誤用這個類。要解決這些問題,我們就必須把确實需要的限制寫出來,這句話說起來簡單,其實做起來不太容易。下面我就來講解一下如何正确的編寫一個規範的限制。
零、簡述
何為限制?所謂限制就是使得編譯器能夠知道 類型參數 除了具備 System.Object 所定義的公共接口外還需要滿足的條件。在建立泛型類型時編譯器必須為這個泛型類型定義有效的 IL 碼,即使它不知道其中的類型參數會在什麼時候替換成什麼類型,也會設法建立出有效的程式集。如果我們不給它指明類型參數,那麼它就會預設設定類型參數是 System.Object 類型。我們通過限制來表達對泛型類型的類型參數的限制要求會營銷編譯器和使用這個類的開發人員。編譯器看到我們指定的限制後就會明白除了除了具備 System.Object 所定義的公共接口外還需要滿足什麼條件。對于編譯器來說它獲得了兩個幫助:
- 可以令編譯器在建立這個泛型類型的時候獲得更多的資訊;
- 編譯器能夠保證使用這個泛型的開發人員所提供的參數類型一定滿足我們所指定的條件。
一、如何規範限制條件
講解之前我們先來看一個例子,這個例子判斷了輸入的兩個值是否相等。
public bool DemoEqual<T>(T t1, T t2)
{
if(t1==null)
{
return t2==null;
}
if(t1 is IComparable<T>)
{
IComparable<T> val1 = t1 as IComparable<T>;
if(t2 as IComparable<T>)
{
return val1.CompareTo(t2)==0;
}
else
{
throw new ArgumentException($"{nameof(t2)} 沒有實作 IComparable<T>");
}
}
else
{
throw new ArgumentException($"{nameof(t1)} 沒有實作 IComparable<T>")
}
}
這段代碼中執行了大量的強類型轉換,在轉換之前還判斷時傳入的參數是否實作了 IComparable 接口。這段代碼如果使用了泛型限制就會很簡單:
public bool DemoEqual<T>(T t1, T t2)
where T : IComparable<T>
=> t1.CompareTo(t2)==0;
這段代碼大大簡化了前面的那段代碼,并且把程式運作期可能出現的錯誤提前到了編譯期,編譯器提前阻止了不符合要求的用法。到這裡你是不是以為上述代碼就是很好的解決方案呢?其實嚴格來說上述代碼矯枉過正了,為什麼這麼說呢?因為 IComparable 接口很常見,大部分開發人員在設計類型的時候都會事先這個接口,是以我們将上述代碼修改一些,我們不使用 CompareTo 來對比兩個值是否相等,我們這次使用 Equals 來對比:
public bool DemoEqual<T>(T t1, T t2)
=> t1.Equals(t2);
上述代碼有一點需要注意,如果 DemoEqual 是定義在泛型類裡,并且泛型類也規定了 IComparable 限制,那麼他調用的 Equals 是 IComparable.Equals ,反之調用的就是 System.Object.Equals 。這兩個 Equals 在性能上沒什麼大的差别,前者的執行效率隻比後者高了那麼一丢丢,因為它隻是不用在運作時檢查程式有沒有重寫 System.Object.Equals ,以及泛型參數類型為值類型時它也不用執行裝箱和拆箱操作。但是對于把性能看的特别重的開發人員來說,前者是最優的方案。
Tip:如果有較好的方法,我還是建議大家使用較好的方法,比如前面我們所說的 IComparable.Equals 。