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)















沒有留言:
張貼留言