2017年9月13日 星期三

C# : 類型成員 Class Member 的實值相等 Value Equality

從前面的文章介紹中,我們知道,關於非基本類型(Primitive Type)的變數,他們的 == 運算子使用的參考相等來比較兩個物件,然而,對於結構型別所產生的物件,預設是無法使用 == 運算子,只能夠使用該執行個體的 Equals 方法來進行與其他結構物件的比較。
在這裡,我們要來了解如何讓您的結構與類別型別所產生的物件,根據您的需求定義以及執行效能的相關考量,實作屬於自己的兩個結構或類別物件的相等比較功能,這包含要實作出 == 與 Equals 這兩個功能。
為了要能夠做出這樣的機制,我們需要進行程式碼的多載 (Overloading)與覆載(Overriding);當然,在 .NET Framework 環境中,已經幫我們預先準備好了簡化這些開發工作,在這裡,我們僅需要實作出 IEquatable介面。
在我們的測試的類別宣告如下程式碼所示:
  • 設定客製化類別,要使用介面 IEquatable<TwoDPoint>
    在這裡,我們使用該泛型介面,要實作出強型別的介面功能。
    因此,我們將要定義這個方法 public bool Equals(TwoDPoint other) 的實際物件比較的行為。在這裡,我們將進行該類別內的資料成員的實際值得比較,已獲得該兩個物件是否具有實值相等。
    在這個方法裏,我們使用 (Object.ReferenceEquals(other, null) 來判斷出這個傳入參數是否為空值,若為空值,當然實值比較就不會成立。
    接著,我們使用這個方法 Object.ReferenceEquals(this, other) 來進行這兩個物件變數,是否指向同一個實體變數記憶體位址,若成立的話,則表示這兩個物件變數具有實值相等。
  • 覆寫 Object.Equals(Object)
    接著,我們需要覆寫這個方法,在類別中定義出 public override bool Equals(object obj) 這個覆寫方法,這個方法執行程式碼相當的簡單,就是執行第一個步驟所實作出來的強型別的泛型介面方法:return this.Equals(obj as TwoDPoint);
  • 覆寫強型別的 == 運算子
    由於,運算子為靜態方法,要執行哪個覆寫運算子方法,會在編譯時期就需要決定出來,因此,我們在這裡需要覆寫這個類別的 == 運算子方法。
    在這裡,我們覆寫了 == 運算子方法 public static bool operator ==,在這個覆寫靜態運算子方法內,我們使用執行個體的 Equals 方法,進行這兩個物件變數的比較 lhs.Equals(rhs)
    當然,我們也是需要在這個方法內檢查是否其中一個物件變數為空值,若成立的話,則這個實值比對就不會成立;不過,在這個需求定義中,若兩個比對的物件變數都是空值 null,則會回傳 true,表示,這次比對是相同的,當然,這個部分,可以依照您當時環境的需求,自行做出調整。
  • 覆寫強型別的 != 運算子
    在這裡,我們也需要覆寫靜態的不等於運算子 public static bool operator !=,在這個強型別靜態方法內,我們會呼叫相等 == 運算子所得到的結果,再進行 ! 布林運算,就會得到不相等的實值比較結果。
  • 覆寫 object.GetHashCode() 方法
    當您有覆寫了 object.Equals() 方法的時候,記得也需要 覆寫 object.GetHashCode() 方法,在這裡,我們簡單使用這兩個物件資料成員的 HashCode 值進行 XOR 的運算,就會得到這個類別執行個體的 HashCode。
class TwoDPoint : IEquatable<TwoDPoint>
{
    public int X { get; set; }
    public int Y { get; set; }
    public TwoDPoint(int x, int y)
    {
        X = x;
        Y = y;
    }

    // 請手動覆寫虛擬 Object.Equals(Object)
    // 輸入 over ,空白按鍵 , 選擇 Equals(object obj)
    public override bool Equals(object obj)
    {
        // 這理由該類別的 Equals 方法來檢查兩個物件是否擁有相當的值
        return this.Equals(obj as TwoDPoint);
    }

    // 實作出 IEquatable<TwoDPoint> 的 特定類型的 Equals 方法
    // 這裡的傳入參數,已經是強型別
    public bool Equals(TwoDPoint other)
    {
        // 若傳入參數為空值 null ,則這次比較不成立
        // 這個必須要先做檢查,免得發生例外異常
        if (Object.ReferenceEquals(other, null))
        {
            return false;
        }

        // 檢查這兩個物件變數是否指向同一個參考物件
        if (Object.ReferenceEquals(this, other))
        {
            return true;
        }

        // 若這兩個物件變數的執行時期型別不相同,則比較不成立
        //if (this.GetType() != other.GetType())
        //{
        //    return false;
        //}

        // 進行該類別物件的欄位實際值的比較
        // 這裡不需要做基底類別的欄位值比較,因為,這個類別的基底類別為
        // System.Object ,該比較預設使用參考比較
        // 若基底類別為客製類別,則需要再呼叫基底類別的 Equals 
        return (X == other.X) && (Y == other.Y);
    }

    // 請手動覆寫虛擬 Object.GetHashCode
    // 輸入 over ,空白按鍵 , 選擇 GetHashCode()
    public override int GetHashCode()
    {
        // return X * 0x00010000 + Y;
        return X.GetHashCode() ^ Y.GetHashCode();
    }

    // 進行相等運算子的多載定義
    // 這裡傳入的參數為強型別
    public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
    {
        // 確認傳入參數不為空值
        if (Object.ReferenceEquals(lhs, null))
        {
            // 確認傳入參數不為空值
            if (Object.ReferenceEquals(rhs, null))
            {
                // 這裡實作出 null == null = true.
                return true;
            }

            // 若傳入參數其中一個為空值,比較不成立
            return false;
        }
        // 執行該物件的成員值比較
        return lhs.Equals(rhs);
    }

    // 進行不相等運算子的多載定義
    public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
    {
        return !(lhs == rhs);
    }
}

進行測試

首先,我們實例化兩個執行個體,分別設定到兩個物件變數內(classObject1, classObject2),接著,設定 classObject2 與 classObject3 同時參考到同一個執行個體(物件上)。
Console.WriteLine("參考型別的物件比較");
TwoDPoint classObject1 = new TwoDPoint(10, 20);
TwoDPoint classObject2 = new TwoDPoint(10, 20);
TwoDPoint classObject3;
classObject3 = classObject2;

Console.WriteLine($"classObject1==classObject2 is {classObject1 == classObject2}");
Console.WriteLine($"classObject1.Equals(classObject2) is {classObject1.Equals(classObject2)}");
Console.WriteLine($"object.Equals(classObject1, classObject2) is {object.Equals(classObject1, classObject2)}");
Console.WriteLine($"object.ReferenceEquals(classObject1, classObject2) is {object.ReferenceEquals(classObject1, classObject2)}");
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
參考型別的物件比較,執行結果如下所示,我們看到了,不論使用了 == 運算子、執行個體覆寫的 Equals 方法、object.Equals 靜態方法與object.ReferenceEquals,都可以確實的進行兩個物件的資料成員的實值相等比較。
參考型別的物件比較
classObject1==classObject2 is True
classObject1.Equals(classObject2) is True
object.Equals(classObject1, classObject2) is True
object.ReferenceEquals(classObject1, classObject2) is False
Press any key for continuing...
接下來,我們要進行參考型別的物件比較(兩個物件變數指向同一個參考物件)
Console.WriteLine("參考型別的物件比較(兩個物件變數指向同一個參考物件)");
Console.WriteLine($"classObject3==classObject2 is {classObject3 == classObject2}");
Console.WriteLine($"classObject1.Equals(classObject2) is {classObject3.Equals(classObject2)}");
Console.WriteLine($"object.Equals(classObject1, classObject2) is {object.Equals(classObject3, classObject2)}");
Console.WriteLine($"object.ReferenceEquals(classObject1, classObject2) is {object.ReferenceEquals(classObject3, classObject2)}");
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
參考型別的物件比較(兩個物件變數指向同一個參考物件)
classObject3==classObject2 is True
classObject3.Equals(classObject2) is True
object.Equals(classObject3, classObject2) is True
object.ReferenceEquals(classObject3, classObject2) is True
Press any key for continuing...

沒有留言:

張貼留言