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
例如: 我們會這樣寫 var datas = customers.OrderBy( x => x.CustomerId);
有些時候, 我們會需要用多個欄位作排序, 當然, 可以一路 OrderBy().ThenBy().... 下去;
以下, 提供另一個方式, 利用實作 IComparer, IComparer
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
7. 參考文件
- 01. CodeProject, Comparing Values for Equality in .NET: Identity and Equivalence
- 02. In 91, [Unit Test Tricks] 如何驗證兩個自訂型別物件集合相等
- 03. In 91, [Unit Test Tricks] Compare Object Equality
- 04. .Net 海角點部落, 結構二三事 (1)
- 05. 石頭的coding之路, Struct V.S Class 兩者之間差異
- 05. Microsoft Docs, IComparable Interface
- 06. Microsoft Docs, IComparable<T> Interface
- 07. Microsoft Docs, IEquatable<T> Interface
- 08. Microsoft Docs, 運算子多載 (C# 參考)
- 09. Microsoft Docs, Enumerable Class
- 10. Microsoft Docs, Lambda 運算式 (C# 程式設計指南)
- 11. Microsoft Docs, Unsafe code and pointers (C# Programming Guide)
沒有留言:
張貼留言