深入理解C#(一)_雙等號跟Equals差異_如用於集合中請再記得覆寫GetHashCode
在日常開發中常碰到的比對數值、字串是否相等
你都是用雙等號 還是 Equals呢?
而這兩者到底有捨麼差別
在此之前必須先有一觀念
於CLR(Common Language Runtime,.NET 提供的執行期間環境)
有定義所謂的「相等性」分為「值相等性」和「引用相等性」
「值相等性」(對數值型別)
若比較兩變數所含的值相等,則定義為值的相等。
在,NET裡面對於string這種一特殊的參考型態,微軟則覺得它更趨近於數值型態。
「引用相等性」(對參考型別)
若筆記的是兩變數引用的是記憶體中相同一個物件,則稱為引用的相等。
一般我們在使用上其實不會有很大差異
但實際上,值類型 和 參考(引用)類型的 Equals比較方式不一樣 !!
因此會看到有人用雙等號有人用Equals
Test Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace App1 { class Program { static void Main(string[] args) { int num1 = 1;//C# 內建關鍵字 對應到System.Int32 struct型別 int num2 = 1; Console.WriteLine(num1 == num2);//true Console.WriteLine(num1.Equals(num2));//true string strVal1 = "apple";//C# 內建關鍵字 對應到System.String Class型別 string strVal2 = "apple"; Console.WriteLine(strVal1 == strVal2);//true Console.WriteLine(strVal1.Equals(strVal2));//true String strObj1 = "banana";//System.String 屬於內建型態(Predefined Type) 這個是Class (Reference Type) String strObj2 = "banana"; Console.WriteLine(strObj1 == strObj2);//true Console.WriteLine(strObj1.Equals(strObj2));//true Console.ReadKey(); } } } |
一般自定義的類別設計層面較會跟string這參考型別類似
(PS:於FLC(.NET Framework Class Library)中,String的比較會是針對於"型別的值",而非針對引用本身的比較。)
String.Equals 方法:
判斷兩個 String 物件是否具有相同的值。
而若用雙等於運算子實際上底層也是間接call String.Equals
以下我們做一個Reference Type的測試
在透過object來測試同樣值都是1的情況
又會發現明明值一樣但雙等號竟然返回false的情況,
因為object num1 , num2各自都為獨立的物件。
1這個值並非透過明確實質型別int來存,而是Boxing成object,因此會變成是在做
object == object之間的比對。
等於等於 ( = = ) 比較運算子:
對於預先定義的實值型別 (Value Type),等號比較運算子 (==) 在運算元相等時傳回 true;否則傳回 false。 對於 string 以外的參考型別,若兩個運算元參考到同一物件,== 會傳回 true。 對於 string 型別,== 會比較字串的值。
換言之,等於等於運算子只有在實際型別(int ,double)或string這種時候用來比較值相等會是有效的,就是我們平常認知的值相等比對,但若是物件跟物件之間的參考型別比對則會發現即便值相同,但參考不同記憶體位址因而返回false這結果。
一個生活化例子
一家公司員工離職後又復職的一個情境
Test Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace App1 { class Program { static void Main(string[] args) { Employee employee1 = new Employee("A0001"); Employee employee2 = new Employee("A0001"); Console.WriteLine(employee1.Equals(employee2));//false employee2 = employee1; Console.WriteLine(employee1.Equals(employee2));//true Console.ReadKey(); } } class Employee { public string WorkerId { get; private set; } public Employee(string WorkId) { this.WorkerId = WorkId; } } } |
會得知參考型別下確實會返回不同即便工號不變,再重新導向記憶體後就又回傳為相同了。
於現實環境中,只要工號一致理論上就代表同一人。
這時候則可透過覆寫Equals的技巧。
於比對時將傳入的object強制轉型為Employee針對WorkerId屬性進行比較。
Test Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace App1 { class Program { static void Main(string[] args) { Employee employee1 = new Employee("A0001"); Employee employee2 = new Employee("A0001"); Console.WriteLine(employee1.Equals(employee2));//true //employee2 = employee1; //Console.WriteLine(employee1.Equals(employee2));//true Console.ReadKey(); } } class Employee { public string WorkerId { get; private set; } public Employee(string WorkId) { this.WorkerId = WorkId; } public override bool Equals(object obj) { //return base.Equals(obj); return this.WorkerId == ((Employee)obj).WorkerId; } } } |
Equals 基本上不太建議自行去覆寫,除非遇到的情境是自訂的型別會用於某個集合的鍵值。
如用於集合中請再記得覆寫GetHashCode
這裡擴充一Class
EmployeeInfo
Test Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace App1 { class Program { static Dictionary<Employee, EmployeeInfo> dicEmpInfo = new Dictionary<Employee, EmployeeInfo>(); static void Main(string[] args) { #region test AddEmp(); Employee employee1 = new Employee("A0001"); Console.WriteLine("Is Contains Info:" + dicEmpInfo.ContainsKey(employee1));//false #endregion Console.ReadKey(); } static void AddEmp() { Employee employee1 = new Employee("A0001"); EmployeeInfo emp1_info = new EmployeeInfo() { EmpFile = @"C:\EmpDoc\A0001" }; dicEmpInfo.Add(employee1, emp1_info); Console.WriteLine("Is Contains Info:" + dicEmpInfo.ContainsKey(employee1));//true } } class Employee { public string WorkerId { get; private set; } public Employee(string WorkId) { this.WorkerId = WorkId; } public override bool Equals(object obj) { //return base.Equals(obj); return this.WorkerId == ((Employee)obj).WorkerId; } } class EmployeeInfo { public string EmpFile { get; set; } } } |
於主程式中會發現返回是false,但明明已經有添加相同值的物件了
原因則在於HashCode的差別
因此除了上述覆寫的Equals
還要再對GetHashCode 覆寫
Test Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Employee { public string WorkerId { get; private set; } public Employee(string WorkId) { this.WorkerId = WorkId; } public override bool Equals(object obj) { //return base.Equals(obj); return this.WorkerId == ((Employee)obj).WorkerId; } public override int GetHashCode() { //return base.GetHashCode(); return this.WorkerId.GetHashCode(); } } |
再次執行就可更精確的得到 True ,Dictionary 也就能正常使用了!
結論:
若比較型別屬於參考的物件型態則需要額外注意或動手腳!!!
其餘數值、字串,只要是以明確實質型態在宣告指派的都能混用比較運算子(雙等號)或Equals
Ref:
Object.Equals 方法
https://docs.microsoft.com/zh-tw/dotnet/api/system.object.equals?view=netcore-3.1
String.Equals 方法
https://docs.microsoft.com/zh-tw/dotnet/api/system.string.equals?view=netcore-3.1
等號比較運算子 (C# 參考)
https://docs.microsoft.com/zh-tw/dotnet/csharp/language-reference/operators/equality-operators
留言
張貼留言