首頁 > 軟體

C#中的==運運算元用法講解

2022-02-18 16:00:46

==運運算元與基元型別

我們分別用兩種方式比較兩個整數,第一個使用的是Equals(int)方法,每二個使用的是==運運算元:

class Program
{
    static void Main(String[] args)
    {
        int num1 = 5;
        int num2 = 5;

        Console.WriteLine(num1.Equals(num2));
        Console.WriteLine(num1 == num2);
    }
}

執行上面的範例,兩個語句出的結果均為true。我們通過ildasm.exe工具進行反編譯,檢視IL程式碼,瞭解底層是如何執行的。

如果您以前從來沒有接觸過IL指令,不過沒關係,在這裡您不需要理解所有的指令,我們只是想了解這兩個比較方式的差異。

您可以看到這樣一行程式碼:

IL_0008:  call       instance bool [mscorlib]System.Int32::Equals(int32)

在這裡呼叫的是int型別Equals(Int32)方法(該方法是IEquatable<Int>介面的實現)。

現在再來看看使用==運運算元比較生成的IL指令:

IL_0015:  ceq

您可以看到,==執行符使用的是ceq指令,它是使用CPU暫存器來比較兩個值。C#==運運算元底層機制是使用ceq指令對基元型別進行比較,而不是呼叫Equals方法。

==運運算元與參照型別

修改上面的範例程式碼,將int型別改為參照型別,編譯後通過ildasm.exe工具反編譯檢視IL程式碼。

class Program
{
    static void Main(String[] args)
    {
        Person p1 = new Person();
        p1.Name = "Person1";

        Person p2 = new Person();
        p2.Name = "Person1";

        Console.WriteLine(p1.Equals(p2));
        Console.WriteLine(p1 == p2);
    }
}

上述C#程式碼的IL程式碼如下所示:

我們看到p1.Equals(p2)程式碼,它是通過呼叫Object.Equals(Object)虛方法來比較相等,這是在意料之中的事情;現在我們來看==運運算元生成的IL程式碼,與基元型別一致,使用的也是ceq指令。

==運運算元與String型別

接來下來看String型別的例子:

class Program
{
    static void Main(String[] args)
    {
        string s1 = "Sweet";
        string s2 = String.Copy(s1);

        Console.WriteLine(ReferenceEquals(s1, s2));
        Console.WriteLine(s1 == s2);
        Console.WriteLine(s1.Equals(s2));
    }
}

上面的程式碼與我們以前看過的非常相似,但是這次我們使用String型別的變數。我們建一個字串,並付給s1變數,在下一行程式碼我們建立這個字串的副本,並付給另一個變數名稱s2

執行上面的程式碼,在控制檯輸出的結果如下:

您可以看到ReferenceEquals返回false,這意味著這兩個變數是不同的範例,但是==運運算元和Equals方法返回的均是true。在String型別中,==運運算元執行的結果與Equals執行的結果一樣。

同樣我們使用過ildasm.exe工具反編譯檢視生成IL程式碼。

在這裡我們沒有看到ceq指令,對String型別使用==運運算元判斷相等時,呼叫的是一個op_equality(string,string)的新方法,該方法需要兩個String型別的引數,那麼它到底是什麼呢?

答案是String型別提供了==運運算元的過載。在C#中,當我們定義一個型別時,我們可以過載該型別的==運運算元;例如,對於以前的例子中我們實現的Person類,如果我們為它過載==運運算元,大致的程式碼如下:

public class Person
{

    public string Name { get; set; }

    public static bool operator ==(Person p1, Person p2)
    {
        // 注意這裡不能使用==,否則會導致StackOverflowException
        if (ReferenceEquals(p1, p2))
            return true;

        if (ReferenceEquals(p1, null) || ReferenceEquals(p2, null))
            return false;

          return p1.Name == p2.Name;
    }

    public static bool operator !=(Person p1, Person p2)
    {
        return !(p1 == p2);
    }
}

上面的程式碼很簡單,我們實現了==運運算元過載,這是一個靜態方法,但這裡要注意的是,方法的名稱是perator==,與靜態方法的相似性;事實上,它們會被由編譯器成一個名稱為op_Equality()的特殊靜態方法。

為了使用事情更加清楚,我們檢視微軟實現的String型別。

在上面的截圖中,我們可以看到,有兩個運運算元的過載,一個用於相等,另一個是不等式運運算元,其運算方式完全相同,但是否定等於運運算元輸出。需要注意的一點是,如果您想過載一個型別的==執行符的實現,那麼您還需要過載!=操作符的實現,否則編譯會報錯。

==運運算元與值型別

在演示值型別的範例前,我們先將Person型別從參照型別改為值型別,Person定義如下:

public struct Person
{
    public string Name { get; set; }

    public Person(string name)
    {
        Name = name;
    }

    public override string ToString()
    {

        return Name;
    }
}

我們將範例程式碼改為如下:

class Program
 {
     static void Main(String[] args)
     {
         Person p1 = new Person("Person1");
         Person p2 = new Person("Person2");

         Console.WriteLine(p1.Equals(p2));
         Console.WriteLine(p1 == p2);
     }
 }

當我們在嘗試編譯上述程式碼時,VS將提示如下錯誤:

根據錯誤提示,我們需要實現Person結構體的==運運算元過載,過載的語句如下(忽略具體的邏輯):

public static bool operator ==(Person p1, Person p2)
 {
 }
 public static bool operator !=(Person p1, Person p2)
 {
 }

當新增上面程式碼後,重新編譯程式,通過ildasm.exe工具反編譯檢視IL程式碼,發現值型別==運運算元呼叫也是op_Equality方法。

關於值型別,我們還需要說明一個問題,在不重寫Equals(object)方法時,該方法實現的原理是通過反射遍歷所有欄位並檢查每個欄位的相等性,關於這一點,我們不演示;對於值型別,最好重寫該方法。

==運運算元與泛型

我們編寫另一段範例程式碼,宣告兩個String型別變數,通過4種不同的方式比較運算:

public class Program
{
    public static void Main(string[] args)
    {
        string str = "Sweet";
        string str1 = string.Copy(str);

        Console.WriteLine(ReferenceEquals(str, str1));
        Console.WriteLine(str.Equals(str1));
        Console.WriteLine(str == str1);
        Console.WriteLine(object.Equals(str, str1));
    }
}

輸出的結果如下:

首先,我們使用ReferenceEquals方法判斷兩個String變數都參照相同,接下來我們再使用實體方法Equals(string),在第三行,我們使用==運運算元,最後,我們使用靜態方法Object.quals(object,object)(該方法最終呼叫的是String型別重寫的Object.Equals(object)方法)。我們得到結論是:

ReferenceEquals方法返回false,因為它們不是同一個物件的參照;String型別的Equals(string)方法返回也是true,因為兩個String型別是相同的(即相同的序列或字元);==運運算元也將返回true,因為這兩個String型別的值相同的;虛方法Object.Equals也將返回true,這是因為在String型別重寫了方法,判斷的是String是否值相同。

現在我們來修改一下這個程式碼,將String型別改為Object型別:

public class Program
{
    public static void Main(string[] args)
    {
        object str = "Sweet";
        object str1 = string.Copy((string)str);

        Console.WriteLine(ReferenceEquals(str, str1));
        Console.WriteLine(str.Equals(str1));
        Console.WriteLine(str == str1);
        Console.WriteLine(object.Equals(str, str1));
    }
}

執行的結果如下:

第三種方法返回的結果與修改之前不一致,==運運算元返回的結果是false,這是為什麼呢?

這是因為==運運算元實際上是一個靜態的方法,對一非虛方法,在編譯時就已經決定用呼叫的是哪一個方法。在上面的例子中,參照型別使用的是ceq指令,而String型別呼叫是靜態的op_Equality方法;這兩個範例不是同一個物件的參照,所以ceq指令執行後的結果是false

再來說一下==運運算元與泛型的問題,我們建立一個簡單的方法,通過泛型方法判斷兩個泛型引數是否相等並在控制檯上列印出結果:

static void Equals<T>(T a, T b)
{
    Console.WriteLine(a == b);
}

但是當我們編譯這段程式碼時,VS提示如下錯誤:

上面顯示的錯誤很簡單,不能使用==運運算元比較兩個泛型T。因為T可以是任何型別,它可以是參照型別、值型別,不能提供==運運算元的具體實現。

如果像下面這樣修改一下程式碼:

static void Equals<T>(T a, T b) where T : class
{
    Console.WriteLine(a == b);
}

當我們將泛型型別T改為參照型別,能成功編譯;修改Main方法中的程式碼,建立兩個相同的String型別,和以前的例子一樣:

public class Program
{
    static void Main(string[] args)
    {
        string str = "Sweet";
        string str1 = string.Copy(str);

        Equals(str, str1);
    }

    static void Equals<T>(T a, T b) where T : class
    {
        Console.WriteLine(a == b);
    }
}

輸出的結果如下:

結果與您預期的結果不一樣吧,我們期待的結果是true,輸出的結果是false。不過仔細思考一下,也許會找到答案,因為泛型的約束是參照型別,==運運算元對於參照型別使用的是參照相等,IL程式碼可以證明這一點:

如果我們泛型方法中的==運運算元改為使用Equals方法,程式碼如下:

static void Equals<T>(T a, T b)
{
    Console.WriteLine(object.Equals(a, b));
}

我們改用Equals,也可以去掉class約束;如果我們再次執行程式碼,控制檯列印的結果與我們預期的一致,這是因為呼叫是虛方法object.Equals(object)重寫之後的實現。

但是其它的問題來了,如果對於值型別,這裡就會產生裝箱,有沒有解決的辦法呢?關於這一點,我們直接給出答案,有時間專門來討論這個問題。

將比較的值型別實現IEquatable<T>介面,並將比較的程式碼改為如下,這樣可以避免裝箱

static void Equals<T>(T a, T b)
{
    Console.WriteLine(EqualityComparer<T>.Default.Equals(a, b));
}

總結

對於基元型別==運運算元的底層機制使用的是ceq指令,通過CPU暫存器進行比較;

對於參照型別==運運算元,它也使用的ceq指令來比較記憶體地址;

對於過載==運運算元的型別,實際上呼叫的是op_equality這個特殊的方法;

儘量保證==操作符過載和Object.Equals(Object)虛方法的寫返回的是相同的結果;

對於值型別,Equals方法預設是通過反射遍歷所有欄位並檢查每個欄位的相等性,為了提高效能,我們需要重寫該方法;

值型別預設情況下不能使用==運運算元,需要實現==運運算元的過載;

由於==運運算元過載實現實際上是一個靜態的方法,在泛型類或方法中使用時與實際的結果可能存在差別,使用Equals方法可以避免這個問題。

到此這篇關於C#中的==運運算元用法講解的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com