在一次記憶體洩漏排查中,我發現問題根源竟然是對 String 類型的錯誤使用。這促使我系統性地重新研究了字串機制,發現許多過去以為理所當然的認知竟然都是錯誤的。這些誤區極具代表性,值得與大家深入探討。
許多開發者認為執行 string s2 = s1 + "e"; 後,s2 會進入字串駐留池。實際上,只有編譯期常量字串才會自動駐留。透過 string.IsInterned 驗證可知,運算時期產生的字串預設並非駐留,除非顯式呼叫 string.Intern 強制駐留。微軟此設計主要基於記憶體效能考量:若所有字串皆駐留,將導致大量記憶體無法釋放。
程式碼 string s1 = "abcd"; s1 = null; 中,許多人以為 "abcd" 會被回收。但因為它是常量字串,屬於駐留字串,**駐留字串的生命週期綁定進程,無法被 GC 回收**。更嚴重的是,若大量使用 string.Intern 強制駐留,即使釋放所有託管物件,這部分記憶體(如 30MB)依然無法釋放。
MSDN 文檔缺失警示: 中文版 MSDN 的
string.Intern說明竟遺漏了關鍵的 "Performance Considerations" 章節。英文版明確指出:駐留字串記憶體直到 CLR 終止才會釋放,且駐留操作本身仍需先分配字串物件記憶體。
習以為常認為 == 比較的是引用,因此引用不同的字串必須用 Equals。但實測發現:
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,引用不同
Console.WriteLine(s3 == s2); // true,值相同
原因在於 C# 對 string 重載了 == 運算子,其 IL 直接呼叫 String.Equals(string, string) 靜態方法,內部先比較引用,再比較值。因此 == 實際上是**值比較**,而非引用比較。
== 比較極快(引用直接相等)。折中方案:在字串構造後嘗試 TryIntern,若已駐留則復用,否則保留原引用。這樣既能提升比較效能,又能避免重複分配相同內容的記憶體。
public static string TryIntern(string str) {
string internStr = string.IsInterned(str);
return internStr == null ? str : internStr;
}
// 使用範例
string s1 = "MyTest";
string s2 = new StringBuilder().Append("My").Append("Test").ToString();
s2 = TryIntern(s2); // 若 "MyTest" 已駐留,s2 將指向駐留實例
注意:TryIntern 本身有效能開銷,僅建議在該字串頻繁比較時使用。