︿
Top

2019年8月14日 星期三

[C#] 如何進行變數或物件的 相同(ReferenceEquals) 或相等(Equals) 的比較: 由 int / string 的原始程式碼談起


0. 前言


在撰寫單元測試時, 常會需要作 expected 與 actual 的比較, 常用的是 Assert.AreSame() 或 Assert.AreEqual().

關於 AreSame() 的部份, 很容易理解, 就是同一個記憶體區塊. 例如:

void Main()
{
    var a = new Customer();
    var b = a;
    Console.WriteLine(Object.ReferenceEquals(a, b));
}
public class Customer
{
    public int Id { get; set; }
    public string Name {get; set; }
}
則 變數 b 與 變數 a 是相同的, 因為指向同一個 Customer 物件.
即 Object.ReferenceEqual(a,b) 會回傳 True.

關於 AreEqual() 的部份, 則會因對於相等的定義不同, 而有不同的結果. 例如:
有 2 個 Box (具有長/寬/高/顏色 4 個屬性), 我們可以定義它的相等是:

  • 長/寬/高 都各自相等即可
  • 長/寬/高 都各自相等之外, 顏色也要相等
因此, 會有不同各自認定的規則, 來確定是否相等.

在 "相等" 的實作方面, 網路上查到了 91 哥的 2 篇文章 (參考文件2 / 參考文件3). 有提到可採用一些的方式作轉換後, 進行比較, 例如:

  • 覆寫 Object 類別的 Equals() / GetHashCode()
  • 轉換成匿名型別, 再作比較
  • 轉換成 Expected Objects, 再作比較

本篇文章的編排比較類似個人的筆記, 說明會放在程式碼裡, 或圖片即能理解, 就不多作說明, 主要內容為:

  • 由 int / string 等內建的資料型別, 如何達到 "相等" 的比較開始談起
  • 再延伸至自訂類別, 如何達到 "相等" 的比較
  • Dictionay<T> 是如何作到去掉重複
  • LINQ 的 OrderBy() 是如何作到的




1. int


1.1 型別定義:

由上圖可以看出 int 本質上是一個 struct. 而 struct 本身具有以下特性:

  • 值型別
  • 存於 Stack 記憶體區塊
  • 只能實作 Interface, 不能繼承
  • 不能是 NULL

而上圖亦呈現, int 係實作 IComparable, IFormattable, IConvertible, IComparable<Int32>, IEquatable<Int32>; 其中, 與 "相等" 有關的是 IComparable, Comparable<Int32>, IEquatable<Int32>, 茲於下述作說明:

1.2 IComparable, IComparable<Int32> :

1.2.1 說明:

這 2 個 interface, 主要是用在 排序 時, 確定大小排列用的.

  • CompareTo(Object value) : 非泛型
  • CompareTo(T value) : 泛型
    • 比 value 小者, 回傳 -1;
    • 比 value 大者, 回傳 1;
    • 與 value 相同者, 回傳 0

1.2.2 範例程式:

void Main()
{
      int i = 5;
      int j= 6;
      int k = 5;
      object o = 4;
      Console.WriteLine(i.CompareTo(j).ToString());
      Console.WriteLine(i.CompareTo(k).ToString());
      Console.WriteLine(i.CompareTo(o).ToString());
      
      //OUTPUT:
      // -1
      // 0
      // 1
}

1.2.3 補充 :
這 2 個 interface 是實作在原來類別裡 (例如: Customer); 另外, 後面會提到 IComparer 與 IComparer<T>, 這 2 個 interface 是另外實作類別的 (例如: CustomerComparer: IComparer<Customer> )

1.3 IEquatable<Int32> :

1.3.1 說明:

上述有2張圖, 第1張是實作 IEquatable<T> 的 Equals() 方法, 第2張是覆寫 Object 類別的 Equals() / GetHashCode() 方法.
這個 interface, 主要是用在 加入 KeyValuePair 的 Collection 時, 用以去除重複 Key 值用的. (例如: Dictionary<T> )

1.3.2 範例程式:

public static void CheckInt()
{
      int i = 5;
      int j = 5;
      int k = i;  //這是值的 assign, 不是位址的 assign
      Console.WriteLine("i.Equals(j): " + i.Equals(j));
      Console.WriteLine("object.Equals(i, j): " + object.Equals(i, j));
      Console.WriteLine("object.ReferenceEquals(i, j): " +  object.ReferenceEquals(i, j));
      Console.WriteLine("object.ReferenceEquals(i, k): " +  object.ReferenceEquals(i, k));
      Console.WriteLine("i == j: " + (i == j));

      //OUTPUT:
      //======= Value Type: int =============
      //i.Equals(j): True
      //object.Equals(i, j): True
      //object.ReferenceEquals(i, j): False     //不同的記憶體位址
      //object.ReferenceEquals(i, k): False     //不同的記憶體位址 k=i 是指 '值' 的指派
      //i == j: True
}

1.3.3 補充:

這個 interface 是實作在原來類別裡 (例如: Box); 另外, 後面會提到 IEqualityComparer<T>, 這個 interface 是另外實作類別的 (例如: ABoxEqualityComparer : IEqualityComparer<ABox>>)



2. string


2.1 類別定義:

而上圖亦呈現, string 係實作 IComparable, ICloneable, IConvertible, IEnumerable, IComparable<String>, IEnumerable<char>, IEquatable<String>; 其中, 與 "相等" 有關的是 IComparable, IComparable<String>, IEquatable<String>, 茲於下述作說明:

2.2 IComparable, Comparable<String> :

由上圖可以發現, 與 int 雷同, 有 2 個 CompareTo() 的方法實作, 其中 1 個是 ICcomparable 界面, 1 個是 ICcomparable<String>.
實作 ICcomparable 界面者, 呼叫 String.Compare() method.
實作 ICcomparable<String> 界者, 呼叫 CultureInfo.Compare()

2.2.1 String.Compare() part #1 :

2.2.2 String.Compare() part #2 :

2.2.3 CultureInfo.Compare() :

關於 unsafe 的說明, 請參考 <參考文件 11>

2.3 IEquatable<String> :

2.4 測試程式 :

public static void CheckString()
{
      string a = "abc";
      string b = "abc";
      string c = a;
      Console.WriteLine("a.Equals(b): " + a.Equals(b));
      Console.WriteLine("object.Equals(a, b): " + object.Equals(a, b));
      Console.WriteLine("object.ReferenceEquals(a, b): " +  object.ReferenceEquals(a, b));
      Console.WriteLine("object.ReferenceEquals(a, c): " +  object.ReferenceEquals(a, c));
      Console.WriteLine("a == b: " + ( a == b) );

      //OUTPUT:
      //======= Reference Type : string =============
      //a.Equals(b): True
      //object.Equals(a, b): True
      //object.ReferenceEquals(a, b): True      //呼叫 object.ReferenceEquals(); 會呼叫 string 多載的 == 運算子
      //object.ReferenceEquals(a, c): True      //呼叫 object.ReferenceEquals(); 會呼叫 string 多載的 == 運算子
      //a == b: True    
}
                    

2.5 補充 : 關於 object.ReferenceEquals(a, b)

A. class Object.ReferenceEquals() 會呼叫 objA == objB
B. class String 有 overloading == 運算子, 會呼叫 String 自訂的 Equals() 方法


3. Box


3.1 類別設計 :

假設有一個類別: Box, 有 長/寬/高/顏色 4 個屬性, 而要判斷 2 個 Box 是否 '相等' 的規則為: "2者的長/寬/高各自相等即可, 不用管顏色".
依前述, 我們可以覆寫(override) Equals() / GetHashCode(); 另外, 因為也可以用 == 運算子來作 2 個物件是否相等的比較; 所以, 我們要來多載(overload) == 及 != 運算子.

public class Box
{
    public int Length { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
    public string Color { get; set; }

    protected bool Equals(Box other)    //這個是我們自己寫的 method, 與 IEquatable<T> 無關; 當然, 也可以改成實作 IEquatable<T>.
    {
        return (Length == other.Length)
                && (Width == other.Width)
                && (Height == other.Height);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Box)obj);    // 呼叫泛型的 Equals() 方法
    }

    public override int GetHashCode()
    {
        int hCode = Length ^ Width ^ Height;    // ^ : XOR 
        return hCode.GetHashCode();
    }

    //overloading == operator
    public static bool operator ==(Box a, Box b)
    {
        if (object.ReferenceEquals(a, null))
        {
            return object.ReferenceEquals(b, null);
        }
        return a.Equals(b);
    }

    //overloading != operator
    public static bool operator !=(Box a, Box b)
    {
        if (object.ReferenceEquals(a, null))
        {
            return object.ReferenceEquals(b, null);
        }
        return !(a.Equals(b));
    }

}
                

3.2 GetHashCode() 說明 :

在加入到 HasshSet<T> 或 Dictionary<T> 才會呼叫 GetHashCode() // defined in IEqualityComparer<T>
GetHashCode() 主要用以初步判别 2 個物件是否相等 (取物件的特徵值).

  • 若不同, 則代表 2 個物件一定不同.
  • 若相同, 還要經過 Equals() 的檢查.

3.3 測試程式 :


public static void CheckBox()
{
    var a = new Box() { Length = 100, Height = 50, Width = 30, Color = "Red" };
    var b = new Box() { Length = 100, Height = 50, Width = 30, Color = "Yellow" };
    var c = a;

    Console.WriteLine("a.Equals(b): " + a.Equals(b));
    Console.WriteLine("object.Equals(a, b): " + object.Equals(a, b));
    Console.WriteLine("object.ReferenceEquals(a, b): " + object.ReferenceEquals(a, b));
    Console.WriteLine("object.ReferenceEquals(a, c): " + object.ReferenceEquals(a, c));
    Console.WriteLine("a == b: " + (a == b));
    //Console.WriteLine(a is string);

    var ds = new Dictionary<Box, string>(); // Key: Box 物件; Value: Box.Color
    var x = new Box() { Length = 10, Height = 5, Width = 3, Color = "Green" };
    ds.Add(a, a.Color);
    try
    {
        ds.Add(b, b.Color); // 重複的 Box 加不進去 Dictioary
    }
    catch (Exception)
    {
        Console.WriteLine("An element with Key = Box already exists.");
    }
    ds.Add(x, x.Color);

    Console.WriteLine("---- elements in Dictionary<Box, string> ----");
    foreach (var element in ds)
    {
        Console.WriteLine($"{element.Key.Length} {element.Key.Width} {element.Key.Height} {element.Key.Color}");
    }

    //OUTPUT:
    //======= Reference Type : Box : override GetHashCode() / Equals() =============
    //a.Equals(b): True
    //object.Equals(a, b): True
    //object.ReferenceEquals(a, b): False
    //object.ReferenceEquals(a, c): True
    //a == b: True
    //An element with Key = Box already exists.
    //---- elements in Dictionary<Box, string> ----
    //100 30 50 Red
    //10 3 5 Green
}
                

4. 案例一: 過濾重複 (class ABox)


4.1 類別設計 :


public class ABox
{
    public int Length { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
    public string Color { get; set; }
}

class ABoxEqualityComparer : IEqualityComparer<ABox>
{
    public bool Equals(ABox b1, ABox b2)
    {
        if (b2 == null && b1 == null)
            return true;
        else if (b1 == null || b2 == null)
            return false;
        else if (b1.Height == b2.Height && b1.Length == b2.Length
                            && b1.Width == b2.Width)
            return true;
        else
            return false;
    }

    public int GetHashCode(ABox bx)
    {
        int hCode = bx.Height ^ bx.Length ^ bx.Width;
        return hCode.GetHashCode();
    }

}
                

4.2 測試程式 :

private static void AddABox(Dictionary<ABox, String> dict, ABox box, String color)
{
    try
    {
        dict.Add(box, color);
    }
    catch (ArgumentException e)
    {
        Console.WriteLine("Unable to add {0}: {1}", box, e.Message);
    }
}

public static void CheckABox()
{
    //註: 實作 IEqualityComparer 只適合用在加入 HashSet<T>, Dictionary<T> 這種 KeyValuePair collection 使用.
    //註: 故下述物件的比對, 只有 c = a 會是 true 
    var a = new ABox() { Length = 100, Height = 50, Width = 30, Color = "Red" };
    var b = new ABox() { Length = 100, Height = 50, Width = 30, Color = "Yellow" };
    var c = a;

    Console.WriteLine("a.Equals(b): " + a.Equals(b));
    Console.WriteLine("object.Equals(a, b): " + object.Equals(a, b));
    Console.WriteLine("object.ReferenceEquals(a, b): " + object.ReferenceEquals(a, b));
    Console.WriteLine("object.ReferenceEquals(a, c): " + object.ReferenceEquals(a, c));
    Console.WriteLine("a == b: " + (a == b));

    Console.WriteLine("---- add to dictionary ----");
    //建立 comparer
    ABoxEqualityComparer comparer = new ABoxEqualityComparer();
    var boxes = new Dictionary<ABox, string>(comparer);
    //加入 3 個 ABox
    var blueBox = new ABox() { Length = 4, Width = 3, Height = 4, Color = "Blue" };
    AddABox(boxes, blueBox, blueBox.Color);
    var yellowBox = new ABox() { Length = 4, Width = 3, Height = 4, Color = "Yellow" };
    AddABox(boxes, yellowBox, yellowBox.Color);
    var greenBox = new ABox() { Length = 3, Width = 4, Height = 3, Color = "Green" };
    AddABox(boxes, greenBox, greenBox.Color);

    foreach (var box in boxes)
    {
        Console.WriteLine($"{box.Key.Length} {box.Key.Width} {box.Key.Height} {box.Key.Color}");
    }

    //OUTPUT:
    //======= Reference Type : ABox : custom IEqualityComparer =============
    //a.Equals(b): False
    //object.Equals(a, b): False
    //object.ReferenceEquals(a, b): False
    //object.ReferenceEquals(a, c): True
    //a == b: False
    //---- add to dictionary ----
    //Unable to add UserQuery+ABox: 已經加入含有相同索引鍵的項目。
    //4 3 4 Blue
    //3 4 3 Green
}
                

5. 案例二: 排序 (class TBox)


5.1 Enumerable.cs 的排序功能簡介 :

在使用 LINQ 進行資料排序的時候, 很自然會想到利用 OrderBy(), ThenBy()... 等方法, 我們會傳入 lanbda expression 作為參數, 指定要依物件的那個屬性, 進行排序.
而 Enumerable.cs 提供一組 static 方法, 用於查詢實作 IEnumerable<T> 的物件.

由下圖, Where() 為一個 IEnumerable<TSource> 的擴充方法 (extension method).

由下圖, 我們可以發現 的 OrderBy() 會用到 IComparer / IComparer, 但通常不需要實作, 因為 OrderBy() 的欄位, 通常是 ValueType 或 string).
例如: 我們會這樣寫 var datas = customers.OrderBy( x => x.CustomerId);

有些時候, 我們會需要用多個欄位作排序, 當然, 可以一路 OrderBy().ThenBy().... 下去;
以下, 提供另一個方式, 利用實作 IComparer, IComparer 的方式, 直接在 OrderBy() 的時候, 指定要採用的 comparer 物件.

5.2 類別設計 :

排序規則: 以長/寬/高 依次排序.

public class TBox
{
    public int Length { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
    public string Color { get; set; }
}


public class TBoxComparer : IComparer, IComparer<TBox>
{
    //IComparer.Compare
    public int Compare(object x, object y)
    {
        if (x is TBox && y is TBox)
        {
            return this.Compare((TBox)x, (TBox)y);
        }
        else
        {
            throw new ArgumentException("傳入參數非 TBox 型別");
        }
    }

    //IComparer<T>.Compare
    public int Compare(TBox x, TBox y)
    {
        // CompareTo() : 欄位值相等的話, 會回傳 0
        if (x.Length.CompareTo(y.Length) != 0)
        {
            return x.Length.CompareTo(y.Length);
        }
        else if (x.Width.CompareTo(y.Width) != 0)
        {
            return x.Width.CompareTo(y.Width);
        }
        else if (x.Height.CompareTo(y.Height) != 0)
        {
            return x.Height.CompareTo(y.Height);
        }
        else
        {
            return 0;
        }
    }
}
                

5.3 測試程式 :


public static void CheckTBox()
{
    var aBoxes = new List<TBox>();

    aBoxes.AddRange(new TBox[]
    {
        new TBox() { Length=300, Width=200, Height=100, Color="Blue" },
        new TBox() { Length=300, Width=150, Height=100, Color="Yellow" },
        new TBox() { Length=200, Width=100, Height=50, Color="Green" },
    }
    );

    //使用 OrderBy(...).ThenBy(...)
    Console.WriteLine("---- 使用 OrderBy(...).ThenBy(...) ----");
    var datas = aBoxes.OrderBy(x => x.Length).ThenBy(x => x.Width).ThenBy(x => x.Height);
    foreach (var data in datas)
    {
        Console.WriteLine($"{data.Length} {data.Width} {data.Height} {data.Color}");
    }

    //使用自訂的 Comparer
    Console.WriteLine("---- 使用自訂的 Comparer ----");
    var datas2 = aBoxes.OrderBy(x => x, new TBoxComparer()); // 將整個 TBox 作為排序的對象, 而非其包含的屬性
    foreach (var data2 in datas2)
    {
        Console.WriteLine($"{data2.Length} {data2.Width} {data2.Height} {data2.Color}");
    }

    //OUTPUT:
    //======= Reference Type : TBox : custom IComparer, IComparer<T> =============
    //---- 使用 OrderBy(...).ThenBy(...) ----
    //200 100 50 Green
    //300 150 100 Yellow
    //300 200 100 Blue
    //---- 使用自訂的 Comparer ----
    //200 100 50 Green
    //300 150 100 Yellow
    //300 200 100 Blue
}

                

6. 結論


針對不同目的, 而必須有不同的作法, 茲整理如下供參考:

  • 物件本身的比較. 作法: override Equals()
  • 物件加入至 KeyValuePair Collection (Dictionary): // 確保 Key 的部份不重複
    A. 由物件著手. 作法: override GetHashCode() / Equals() + overload == / !=
    B. 自訂 comparer 類別, 實作 IEqualityComparer. 作法: 實作 GetHashCode() / Equals()
  • 排序:
    A. 自訂 comparer 類別, 實作 IComparer, IComparer<T>. 作法: 實作各自的 Compare() method

沒有留言:

張貼留言