C#中==操作符存在的缺陷
==操作符因為語法簡潔而備受歡迎,但它本身也存在著局限性,比如繼承或泛型問題。下面讓我們依次來看看吧。
1、==和繼承性問題
關於==運算符在繼承時存在的問題,我們以String類型為例進行說明。
static void Main(string[] args) { string str = "hello"; 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)); Console.Read(); }
運行上面代碼,依次產生:False、True、True、True。該結果很容易解釋,除了ReferenceEquals方法進行引用比較之外,其他三種比較方法均是值比較。
現在讓我們稍微修改一下上述代碼,如下所示:
static void Main(string[] args) { Object str = "hello"; Object str1 = string.Copy(str); Console.WriteLine(ReferenceEquals(str, str1)); Console.WriteLine(str.Equals(str1)); Console.WriteLine(str == str1); Console.WriteLine(object.Equals(str, str1)); Console.Read(); }
再次運行上面代碼,結果為:False、True、False、True。和上面的結果進行比較,可以發現,只有==操作符的結果發生了改變,這是為什麽呢?
原因就在於==相當於一個靜態方法,而靜態方法不可能是virtual的,本例中當用==進行比較時,比較的是兩個Object類型的變量,盡管我們知道str和str1實際是String類型的,但編譯器並沒有意識到這一點。我們應該牢記的一點就是:對於非虛方法的調用而言,具體調用哪個實現是在編譯時期就已經做出決定了。具體到我們的例子,就是說,我們聲明了Object類型的兩個變量str和str1,那麽編譯器就會生成比較Object類型的代碼。而Object類中是沒有==操作符的重載版本實現的,因此,==將進行引用相等性比較,因str和str1是兩個不同的實例對象,故返回False。
由於在面臨繼承時的無能為力,故此是不應選擇==,而應使用Equals方法進行判等。接下來看一下Equals方法是如何解決這個問題的。
顯然,當使用Equals方法進行判等測試時,無論調用的是str.Equals還是object.Equals方法,最終調用的都是String類型的override版本實現,故總能計算出我們預期的結果。因此,在存在繼承問題時,應使用Equals方法進行判等,而不是==操作符。
最後,從本例中可以看到,當我們將==操作符的操作數強轉到Object類型時,它將進行引用相等性測試,總是和ReferenceEquals的結果保持一致,因此,一些開發者就使用這種方式來比較引用相等性,但這樣做的一個缺點在於:其他的開發者在讀到這樣的代碼片段時可能產生疑惑。因此在比較引用相等性時最好總是使用ReferenceEquals方法。
2、==和泛型問題
==的另一個缺陷就是不能和泛型很好地工作在一起。考慮下面的代碼:
static void Equals<T>(T a, T b) { Console.WriteLine(a == b); }
上面代碼的邏輯很簡單,就是使用==比較兩個T類型的對象。但是編譯上述代碼將報錯:
之所以報上面的錯誤,是因為T可能代表任意類型,包括基元類型、值類型和引用類型。無法確定傳遞的類型是否實現了==操作符重載。
在C#中,對於泛型類型,我們無法施加這樣的約束,即強制要求傳入的泛型類型T實現==的重載。
現在,我們能夠構建代碼,僅有的一個問題就是:Equals被限定在僅能接受引用類型,而不能接受值類型。
現在,讓我們還是以之前的字符串比較為例,
class Program { static void Main(string[] args) { Object str = "hello"; Object str1 = string.Copy((string)str); Equals(str, str1); } static void Equals<T>(T a, T b) where T : class { Console.WriteLine(a == b); } }
現在,猜測一下上面代碼的輸出結果,是true還是false ?如果我們回想起String類型定義了==操作符的重載實現,那麽很可能猜測上面代碼的結果為true,但實際運行結果卻顯示false,這是為何呢?此時很直觀的猜測就是==操作符計算的是引用判等,而非值判等。下面讓我們看看究竟發生了什麽。
上面的代碼中,盡管通過where T : class語句限定使得編譯器知道它能對傳入的任何類型應用==操作符進行判等性測試,對應到本例T就是String類型,而且String類型提供了==的重載實現,但編譯器並不曉得泛型類型T是否重載了==操作符,因此,它假定T沒有重載==。編譯器編譯代碼時假定調用的是Object基類==操作符,因此,==操作符進行引用判等性測試。
應該始終牢記一點:在對泛型類型T使用==操作符時,編譯器不會使用類型T定義的==運算符重載,相反,它會將T視為Object類型,並調用Object基類的==操作符方法。
接下來讓我們看下Equals方法如何解決上面的問題。
static void Equals<t>(T a, T b) { Console.WriteLine(object.Equals(a,b)); }
可以看出,我們移除了泛型方法的class限定語句,因為能在任何類型上調用Object.Equals方法,之所以使用static限定,是為了避免發出調用的實例對象為null,可以看出上面的泛型方法對值類型和引用類型均奏效。
我們再次運行上面的代碼,結果顯示True。這是因為Object.Equals方法在運行時將調用它的override版本實現,本例中override版實現的定義位於String類型中,該實現進行值判等性測試。
C#中==操作符存在的缺陷