今天在看C#編程指南時(類型參數的約束http://msdn.microsoft.com/zh-cn/library/d5x73970.aspx)看到一段描述:
在應用 where T : class 約束時,避免對類型參數使用 == 和 != 運算符,因為這些運算符僅測試引用同一性而不測試值相等性。即使在用作參數的類型中重載這些運算符也是如此。下面的代碼說明了這一點;即使 String 類重載 == 運算符,輸出也為 false。
並給出的代碼
public static void OpTest<T>(T s, T t) where T : class { System.Console.WriteLine(s == t); } static void Main() { string s1 = "target"; System.Text.StringBuilder sb = new System.Text.StringBuilder("target"); string s2 = sb.ToString(); OpTest<string>(s1, s2); }
返回的結果為False即s1!=s2
到這里都十分容易理解,因為在C#中==運算符對於值類型,如果對象的值相等,則相等運算符 (==) 返回 true,否則返回 false。對於引用類型,如果兩個對象引用同一個對象,則 == 返回 true。
所以如果我將代碼改為:
static void Main() {
string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = sb.ToString();
s2 = s1;
OpTest<string>(s1, s2); }
返回的結果為TRUE即s1==s2,因為s1和s2引用同一個對象。
接下來我又做了一個實驗(重點來了):
static void Main() { string s1 = "target"; string s2 = "target"; OpTest<string>(s1, s2); }
返回的結果依然是TRUE即s1==s2,這個結果完全出乎我的預料!思考中......
==這里其實有一個很大的陷阱!上面的理解有一處很大的破綻!因為筆者忘記了很重要的一條!
在C#參考中有以下描述(== 運算符(C# 參考)http://127.0.0.1:47873/help/1-30636/ms.help?product=VS&productVersion=100&method=f1&query=%3D%3D_CSharpKeyword%00%3D%3D&locale=zh-CN&category=DevLang%3acsharp%00TargetFrameworkMoniker%3a.NETFramework,Version%3Dv4.0)
對於預定義的值類型,如果操作數的值相等,則相等運算符 ( ==) 返回 true,否則返回 false。 對於 string 以外的引用類型,如果兩個操作數引用同一個對象,則 == 返回 true。 對於 string 類型, == 比較字符串的值。
也就是說string類型其實有對==運算符進行重載(實時上作者在剛開始閱讀C#編程指南時明顯理解錯誤),實時如此:
string s1 = "target"; System.Text.StringBuilder sb = new System.Text.StringBuilder("target"); string s2 = sb.ToString(); if (s1 == s2) {
System.Console.WriteLine("=="); } else {
System.Console.WriteLine("!="); }
結果顯示s1 == s2 返回True,而在本文開頭提出的C#編程指南時(類型參數的約束)例子中講的就是雖然string有對==運算符進行重載,但是在范式編程中會被無視,所以要慎用(好吧,筆者的基本功堪憂啊!),那么為啥
string s1 = "target"; string s2 = "target"; OpTest<string>(s1, s2);
會得到s1==s2的結果?
讓我們換個思考方式:
1.在OpTest<string>(s1, s2);中==判斷的是兩個操作數是否引用的同一個對象,如果是則返回True,反之False(包含string類型)
2.在上述例子中s1==s2,則證明s1和s2引用的是同一個對象
於是筆者又嘗試了
string[] str = new string[3]; str[0] = "123"; str[1] = "123"; str[2] = "123"; OpTest<string>(str[0], str[2]);
string str0 = @"D:\mysher"; string str1 = "D:\\mysher"; OpTest<string>(str0, str1);
等到的結果都是兩個string變量引用了同一個對象
所以筆者得到結論當有多個字符串變量包含了同樣的字符串實際值時,CLR讓它們向同一個字符串對象。
那么既然是這樣,Microsoft在C#編程中給出的例子
string s1 = "target"; System.Text.StringBuilder sb = new System.Text.StringBuilder("target"); string s2 = sb.ToString(); OpTest<string>(s1, s2);
s1和s2也應該滿足條件,指向同一個對象啊,可事實卻相反。
后來筆者在園子里找到了答案,感謝cyoooo7給出了十分詳細的解釋:
CLR默默地維護了一個叫做駐留池(Intern Pool)的表。這個表記錄了所有在代碼中使用字面量聲明的字符串實例的引用。這說明使用字面量聲明的字符串會進入駐留池,而其他方式聲明的字符串並不會進入,也就不會自動享受到CLR防止字符串冗余的機制的好處了。但是在不使用字面量聲明時,如CLR在為ToString()方法的返回值分配內存時,並不會到駐留池中去檢查是否字符串已經存在了,所以會重新分配內存。
為了讓編程者能夠強制CLR檢查駐留池,以避免冗余的字符串副本,String類的設計者提供了一個名為Intern的類方法,如下
string s1 = "target"; System.Text.StringBuilder sb = new System.Text.StringBuilder("target"); string s2 = String.Intern(sb.ToString()); OpTest<string>(s1, s2);
這樣s1和s2又指向同一個對象了。
如果有興趣的朋友可以去看看cyoooo7的《原來是這樣:C#中字符串的內存分配與駐留池》一文。