規範約束條件

我們在開發時往往會對泛型指定約束條件,只有類型參數符合條件的才允許用在這個泛型上面。但是有時我們會定義過多或過少的約束條件,過多的約束條件會導致其他開發人員在使用你所編寫的方法或類時做很多的工作以滿足這些約束,過少的約束又會導致程序在運行的時候必須做很多的檢查,並執行更多的強制類型轉化操作,有時我們還需要使用反射生成運行期錯誤,來防止用戶誤用這個類。要解決這些問題,我們就必須把確實需要的約束寫出來,這句話說起來簡單,其實做起來不太容易。下面我就來講解一下如何正確的編寫一個規範的約束。

零、簡述

何爲約束?所謂約束就是使得編譯器能夠知道 類型參數 除了具備 System.Object 所定義的公共接口外還需要滿足的條件。在創建泛型類型時編譯器必須爲這個泛型類型定義有效的 IL 碼,即使它不知道其中的類型參數會在什麼時候替換成什麼類型,也會設法創建出有效的程序集。如果我們不給它指明類型參數,那麼它就會默認設置類型參數是 System.Object 類型。我們通過約束來表達對泛型類型的類型參數的約束要求會營銷編譯器和使用這個類的開發人員。編譯器看到我們指定的約束後就會明白除了除了具備 System.Object 所定義的公共接口外還需要滿足什麼條件。對於編譯器來說它獲得了兩個幫助:

  1. 可以令編譯器在創建這個泛型類型的時候獲得更多的信息;
  2. 編譯器能夠保證使用這個泛型的開發人員所提供的參數類型一定滿足我們所指定的條件。

一、如何規範約束條件

講解之前我們先來看一個例子,這個例子判斷了輸入的兩個值是否相等。

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 。

我們在編寫泛型類的時候,最好在內部編寫相互重載的多個方法,這樣就可以針對不同的情況調用不同的方法,並且其他開發人員調用起來也不會有過於嚴謹的約束。有時候我們定義的約束過於嚴謹,會導致泛型類的適用範圍很狹窄,遇到這種情況時我們就應該考慮我們自己在泛型類種編寫代碼來判斷傳入的類型是否繼承自某個類或者實現了某個接口。在泛型約束中有三種約束我們必須謹慎使用,它們就是 new 、 struct 以及 class 約束,因爲它們會限定對象的構建方式,除非你要求對象的默認值必須是 0 、null 或者必須能以 new() 的形式創建,那麼我們纔可以使用這三種約束。

二、總結

約束是爲了向調用方提出要求,但是如果約束太多調用方就需要做更多的工作來滿足這些約束,因此在創建約束時應該權衡利弊,將多餘的約束去掉只保留需要的約束。

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