C#效率優化(1)-- 使用泛型時避免裝箱

  本想接着上一篇詳解泛型接着寫一篇使用泛型時須要注意的一個性能問題,可是後來想着不如將以前的詳解XX系列更正爲如今的效率優化XX系列,記錄在工做時遇到的一些性能優化的經驗和技巧,若是有什麼不足,還請你們多多指出;性能優化

  在使用集合時,一般爲了防止裝箱操做而選擇List<T>、Dictionary<TKey, TValue>等泛型集合,可是在使用過程當中若是使用不當,依然會產生大量的裝箱操做;ide

  首先,將值類型的實例當作引用類型來使用時,即會產生裝箱,例如:性能

int num = 10;
object obj = num;
IEquatable<int> iEquatable = num;

  其次,對於自定義結構,在正常使用時,一般須要注意一些誤裝箱的操做:優化

public struct MyStruct
{
   public int MyNum;
}

  對該結構MyStruct的實例調用基類Object中的方法時,都會進行裝箱操做,對於靜態方法(Equals、ReferenceEquals)很好理解,對於實例方法,在CLR調用實例方法時,實際上會把調用這個方法的對象看成第一個參數傳入實例方法,而基類Object中的實例方法都會將Object類型的對象做爲第一個參數,所以也會發生裝箱,這其中的實例方法包括GetType和虛方法Equals、GetHashCode、ToString;this

  其中,GetType方法自己就是經過堆內存中與實例數據一塊兒存儲的類型對象指針來獲取實例類型信息的,對於值類型實例,自己就沒有這個開銷成員,此處應使用typeof()運算符代替避免裝箱;spa

  三個虛方法能夠經過在MyStruct中重寫來防止裝箱操做;可是對於Equals方法,有一些須要區別注意的地方:指針

  在調用值類型基類ValueType中的ValueType.Equals(object obj)方法進行比較操做時,會對當前實例和實參obj進行裝箱,共兩次裝箱(抽象基類ValueType依然是類類型);在MyStruct中重寫了該方法MyStruct.Equals(object obj),在調用myStruct1.Equals(myStruct2)時,依然會對myStruct2進行裝箱,共一次裝箱,此時咱們能夠在MyStruct中聲明一個Equals的重載方法,參數類型一樣爲MyStruct,同時對==和!=運算符進行重載:code

public struct MyStruct
{
    public int MyNum;
    public override bool Equals(object obj)  //調用時會對實參進行裝箱
    {
        if (!(obj is MyStruct))
        {
            return false;
        }
        MyStruct other = (MyStruct)obj;  //拆箱
        return this.MyNum == other.MyNum;
    }
    public bool Equals(MyStruct other)  //重載Equals方法,避免裝箱
    {
        return this.MyNum == other.MyNum;
    }
    public static bool operator ==(MyStruct left, MyStruct right)  //比較時一般採用==運算符
    {
        return left.Equals(right);
    }
    public static bool operator !=(MyStruct left, MyStruct right)
    {
        return !(left == right);
    }
}

  此時,在調用myStruct1.Equals(myStruct2)、myStruct1 == myStruct二、myStruct1 != myStruct2時都再也不產生裝箱操做;對象

  可是,在使用泛型方法時,例如對於如下的方法,重載方法並不會生效:blog

static bool MyFunc<T>(T obj1, T obj2)
{
    return obj1.Equals(obj2);
}

  查看其生成的IL代碼能夠清楚的知道不生效的緣由:

  其中默認對obj2進行了box指令調用,而對於obj1,在調用callvir指令時加入了前綴constrained指令,則會判斷obj1的類型定義中是否存在Equals方法的重寫,若是有則調用重寫方法,若是沒有,則裝箱後調用基類ValueType中的虛方法;前面MyStruct的定義中重寫了Equals方法,所以會調用該重寫方法,此時只觸發一次對obj2的裝箱,但依然不是咱們想要的;

  爲了不這個問題,咱們須要在MyStruct的定義中實現IEquatable<T>接口,並在這個泛型方法的聲明中添加約束:

public struct MyStruct : IEquatable<MyStruct>
{
    public int MyNum;
    public override bool Equals(object obj)
    {
        if (!(obj is MyStruct))
        {
            return false;
        }
        MyStruct other = (MyStruct)obj;
        return this.MyNum == other.MyNum;
    }
    public bool Equals(MyStruct other)  //實現IEquatable<T>接口中的方法
    {
        return this.MyNum == other.MyNum;
    }
    public static bool operator ==(MyStruct left, MyStruct right)
    {
        return left.Equals(right);
    }
    public static bool operator !=(MyStruct left, MyStruct right)
    {
        return !(left == right);
    }
}
static bool MyFunc<T>(T obj1, T obj2) where T : IEquatable<T>
{
      return obj1.Equals(obj2);
}

  此時,查看其IL代碼,能夠發現沒有了box指令,避免了裝箱操做:

  對泛型集合List<Mystruct>使用一些內含比較的實例方法時,也會遇到上面的裝箱問題,解決方法一樣是實現IEquatable<T>接口;以經常使用的Contains方法舉例:

  List<MyStruct>中的Contains方法中會調用泛型抽象類EqualityComparer<T>.Default的實例來進行比較,而在抽象類EqualityComparer<T>中,會根據類型參數T實例化對應的具體類實例,具體可查看EqualityComparer<T>.CreateComparer()中的實例生成邏輯,其中,會根據T是否實現了IEquatable<T>接口而實例化不一樣的類的實例:

internal class GenericEqualityComparer<T>: EqualityComparer<T> where T: IEquatable<T>
internal class ObjectEqualityComparer<T>: EqualityComparer<T>

  這兩個類的具體實現這裏再也不贅述;

  基於上面的理解,對於值類型,實現基類的虛方法和IEquatable<T>接口對於避免裝箱十分有必要;

 


若是您以爲閱讀本文對您有幫助,請點一下「推薦」按鈕,您的承認是我寫做的最大動力!

做者:Minotauros
出處:https://www.cnblogs.com/minotauros/

本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。

相關文章
相關標籤/搜索