昨天調試一段程式發現記憶體始終釋放不掉,最後終于發現是對String 的錯誤使用造成,這促使我今天又仔細研究了一下String類型,不研究不知道,一研究發現我過去對String 的很多認識都是錯誤的,感覺這種錯誤認識還比較有典型性,于是寫下此文和大家一起探讨。
1. String 類型變量追加,或修改後的新String對象是駐留(Interned)的。
如下面代碼

string s1 = "abcd";

string s2 = s1 + "e";

我過去想當然的認為s2 是駐留的,但實際上并非如此,用 string.IsInterned 方法檢測s2是非駐留的。後來研究發現隻有常量字元串才會預設駐留,其他的字元串變量哪怕是采用 new string 構造出來的,預設都非駐留,除非用string.Intern 強行駐留。後面我将提到駐留對記憶體的影響,微軟之是以不讓所有的字元串都駐留,我認為還是處于記憶體方面的考慮。
2. String 變量不再引用後CLR會通過GC自動釋放其記憶體。


s1 = null;

上面代碼,我想當然的認為s1 = null 後已經不再對 "abcd" 這個字元串引用,如果沒有其他引用指向這個字元串,GC會釋放"abcd"這塊記憶體。實際結果卻是否定的。因為s1 被賦予了一個常量,導緻 "abcd"這個字元串是駐留的,駐留的字元串在程序結束之前無法被自動釋放。更糟糕的是,我昨天調試的那段程式裡面大量的字元串變量被采用 string.Intern 強制駐留,這導緻我把所有的托管對象都釋放了依然無法釋放那部分大概30多M的記憶體。
遺憾的是微軟的MSDN中文版中string.Intern 的幫助資訊裡面竟然漏掉了性能考諒(Performance consideration) 這一節,我估計大多數中國程式員包括我在内如果有中文的幫助是懶得去看英文的。很遺憾微軟中文的幫助不知道為什麼把最重要的部分給漏了。下面是英文幫助中Performance consideration 一節。
看了英文的幫助就知道Intern 後的字元串是無法釋放的了。
3. 兩個String如果引用不同隻能用Equal 比較。
我一直想當然的認為 兩個String 類型如果用 == 操作符比較,将比較其引用。是以如果兩個String引用不同,則隻能使用Equal 來比較它們是否相等。
比如下面語句

string s2 = new StringBuilder().Append("My").Append("Test").ToString();

string s3 = new StringBuilder().Append("My").Append("Test").ToString();
如下方法比較其引用
Console.WriteLine((object)s3 == (object)s2);
得到結果為 false,即s2, s3指向不同引用。
那麼我想當然的認為 Console.WriteLine(s3 == s2); 的結果也是false,因為string 是引用類型,用==操作符比較引用類型變量,如果兩個變量的引用不同,即便值相同,也會傳回false. 然而運作的結果讓我大跌眼鏡。傳回的值是true.
于是在網上狂搜,最後終于找到了原因。
String 的等号操作符的處理是特殊的,其源碼如下
=== Equality operator on string type (C#) ===

// The == operator overload MSIL:

.method public hidebysig specialname static bool

op_Equality(string a, string b) cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldarg.1
L_0002: call bool System.String::Equals(
string, string)
L_0007: ret
}
從這段源碼中我們看到.net 在字元串等号操作符中調用了 System.String::Equals 這個靜态方法來比較。這個靜态方法的代碼如下。


// Determines whether two Strings match.

public static bool Equals(String a, String b)
if ((Object)a==(Object)b)
{
return true;
}
if ((Object)a==null || (Object)b==null)
return false;
return EqualsHelper(a, b);
}
從這個代碼我們可以看出兩個string 類型在進行==操作符比較時先比較引用是否相等,如果不等會調用EqualsHelper比較值是否相等。這也就是我們看到用==操作符比較兩個引用不同但值相同的string時得到true的原因。
一點建議
從時間角度考慮性能,如果字元串是駐留的,那麼用==操作符比較起來,在被比較的兩個字元串相等的情況下将會非常快。但從空間效率考慮,
如果對所有字元串都駐留,勢必導緻大量記憶體無法被釋放。折中一下,可以在構造字元串後進行如下操作。這樣構造出來的字元串如果
已經駐留,則使用駐留後的字元串引用,否則使用原來引用,這樣除了可以提高比較的效率還可以減少記憶體的開銷,因為該字元串之前已經被駐留過了,
我們沒有必要再重新申請其它的記憶體來存儲相同的字元串。 當然調用TryIntern本身會有一些性能損失,是以還要視具體情況使用,如果該字元串構造出來後
被頻繁用于比較,則在第一次構造時使用TryIntern損失一些性能是值得的,否則就不值得,建議直接使用構造出來的字元串。

string s1 = "MyTest";



s2 = TryIntern(s2);


public static string TryIntern(string str)
string internStr = string.IsInterned(str);
return internStr == null? str: internStr;