深入理解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

留言

這個網誌中的熱門文章

何謂淨重(Net Weight)、皮重(Tare Weight)與毛重(Gross Weight)

Architecture(架構) 和 Framework(框架) 有何不同?_軟體設計前的事前規劃的藍圖概念

經得起原始碼資安弱點掃描的程式設計習慣培養(五)_Missing HSTS Header