天天看點

差別和認識C#中四個判等函數

.Net有四個判等函數?不少人看到這個标題,會對此感到懷疑。事實上确是如此,.Net提供了ReferenceEquals、靜态Equals,具體類型的Equals以及==操作符這四個判等函數。但是這四個函數之間有細微的關系,改變其中一個函數的實作會影響到其他函數的操作結果。

首先要說的是Object.ReferenceEquals和Object.Equals這兩個靜态函數,對于它們倆來說,是不需要進行重寫的,因為它們已經完成它們所要得做的操作。

對于Object.ReferenceEquals這個靜态函數,函數形勢如下:

public static bool ReferenceEquals( object left, object right );

這個函數就是判斷兩個引用類型對象是否指向同一個位址。有此說明後,就确定了它的使用範圍,即隻能對于引用類型操作。那麼對于任何值類型資料操作,即使是與自身的判别,都會傳回false。這主要因為在調用此函數的時候,值類型資料要進行裝箱操作,也就是對于如下的形式來說。

    int n = 10;

    Object.ReferenceEquals( n, n );

這是因為對于n這個資料裝箱兩次,而每次裝箱後的位址有不同,而造成Object.ReferenceEquals( n, n )的結果永遠為false。

對于第一個判等函數來說,沒有什麼好擴充的,因為本身已經很好地完成了它所要做的。

對于第二個Object.Equals這個靜态函數,其形式如下:

public static bool Equals( object left, object right );

按照書中對它的分析,其大緻函數代碼如下:

    public static void Equals( object left, object right )

    {

        // Check object identity

        if( left == right )

            return true;

        // both null references handled above

        if( ( left == null ) || ( right == null ) )

            return false;

        return left.Equals( right );

    }

可以說,Object.Equals這個函數完成判等操作,需要經過三個步驟,第一步是需要根據對象所屬類型的==操作符的執行結果;第二步是判别是否為null,也是和第一步一樣,需要根據類型的==操作符的執行結果;最後一步要使用到類型的Equals函數的執行結果。也就是說這個靜态函數的傳回結果,要取決于後面要提到的兩個判等函數。類型是否提供相應的判等函數,成為這個函數傳回結果的重要因素。

那麼對于Object.Equals這個靜态方法來說,雖說接受參數的類型也屬于引用類型,但是不同于Object.ReferenceEquals函數,對于如下的代碼,能得出正确的結果。

    int n = 10;

    Debug.WriteLine( string.Format( "{0}", Object.Equals( n, n ) ) );

    Debug.WriteLine( string.Format( "{0}", Object.Equals( n, 10 ) ) );

這是因為在此函數中要用到具體類型的兩個判等函數,不過就函數本身而言,該做的判斷都做了,是以不需要去重載添加複雜的操作。

為了更好的述說剩下兩個函數,先解釋一下等價的意義。對于等價的意義,就是自反、對稱以及傳遞。

所謂自反,即a == a;

而對稱,是a == b,則b == a;

傳遞是 a == b,b == c,則 a == c;

了解等價的意義後,那麼在實作類型的判等函數也要滿足這個等價規則。

對于可以重載的兩個判等函數,首先來介紹的是類型的Equals函數,其大緻形式如下:

public override bool Equals( object right );

那麼對于一個類型的Equals要做些什麼操作呢,一般來說大緻如下:

    public class KeyData

    {

        private int nData;

        public int Data

        {

            get{ return nData;}

            set{ nData = value; }

        }

        public override bool Equals( object right )

        {

            //Check null

            if( right == null )

                return false;

            //check reference equality

            if( object.ReferenceEquals( this, right ) )

                return true;

            //check type

            if( this.GetType() != right.GetType() )

                return false;

            //convert to current type

            KeyData rightASKeyData = right as KeyData;

            //check members value

            return this.Data == rightASKeyData.Data;

        }

    }

如上增加了一個類型檢查,即

if( this.GetType() != right.GetType() )

這部分,這是由于子類對象可以通過as轉化成基類對象,進而造成不同類型對象可以進行判等操作,違反了等價關系。

除此外對于類型的Equals函數來,其實并沒有限制類型非要屬于引用類型,對于值類型也是可以重載此函數,但是我并不推薦,主要是Equals函數的參數類型是不可變的,也就是說通過此方法,值類型要經過裝箱操作,而這是比較影響效率的。

而對于值類型來說,我推薦使用最後一種判等函數,即重載運算符==函數,其大緻形式如下:

public static bool operator == ( KeyData left,  KeyData right );

對于一個值類型而言,其的大緻形式應該如下:

    public struct KeyData

    {

        private int nData;

        public int Data

        {

            get{ return nData;}

            set{ nData = value; }

        }

        public static bool operator == ( KeyData left,  KeyData right )

        {

            return left.Data == right.Data;

        }

        public static bool operator != ( KeyData left, KeyData right )

        {

            return left.Data != right.Data;

        }

    }

由于==操作與!=操作要同步定義,是以在定義==重載函數的時候,也要定義!=重載函數。這也是.Net在判等操作保持一緻性。那麼對于最後一個判等函數,這種重載運算符的方法并不适合引用類型。這就是.Net經常現象,去判斷兩個引用類型,不要用==,而要用某個對象的Equals函數。是以在編寫自己類型的時候,要保留這種風格。

那麼對于以上介紹的四種判等函數,會産生如下類似的對比表格。

操作結果取決于 适用範圍 建議
Object.ReferenceEquals 兩個參數對象是否屬于同一個引用 引用類型 不要用它來判斷值類型資料
Object.Equals 參數類型自身的判等函數 無限制 考慮裝箱操作對值類型資料産生的影響
類型的Equals 類型重載函數 無限制 考慮裝箱操作對值類型資料産生的影響
類型的==重載 類型重載函數 無限制 不要在引用類型中重載此運算符

那麼在編寫類型判等函數的時候,要注意些什麼呢,給出如下幾點建議。

首先,要判斷目前定義的類型是否具有判等的意義;

其次,定義類型的判等函數要滿足等價規則;

最後一點,值類型最好不要重載定義Equals函數,而引用類型最好不要重載定義==操作符。

對于string這個引用類型是非常特殊一個引用類型。

它有兩點特殊的地方。

第一點對象配置設定的特殊。

例如:

string str1 = "abcd";

string str2 = "abcd";

那麼.net在配置設定string類型的時候,先檢視目前string類型清單是否有相同的,如果有的話,直接傳回其的引用,否則重新配置設定。

第二點對象引用操作的特殊,可以說不同于真正意義上的引用操作。

例如:

string str1 = "abcd";

string str2 = str1;

str2 = "efgh";// str1 is still "abcd" here

當對于一個新的string類型是原有對象引用的時候,這點和一般的引用類型一樣,但是當新的對象發生變化的時候,要重新配置設定一個新的地方,然後修改對象指向。

是以對于string操作的時候,尤其發生變化的時候,會顯得比較慢,因為其牽扯到記憶體位址的變化。

對于資料量比較大的字元操作時候,使用StringBuilder來說效率會提升很高。