C# 開發人員應該都非常熟悉 System.String 這個型別,String 應該是在 .NET 裡很重要且經常被使用的型別了。依據 MSDN 上的解釋,String 是表示一組依順序存放的字元集合 (a sequential collection of characters),並且每個字元都是 UTF-16 的 Code Unit。
在 .NET 裡 String 提供了許多函式,如 Remove 或 Replace 等可以快速改變字串的值。另外 ToCharArray 函式則會回傳一個字元 Char 陣列,所以 String 字串在內部是以 Char 陣列的方式存放。雖然我們很常使用 System.String 型別,但其實 String 是一個很特殊的類別。
String Interning
String interning 幾乎是現代程式語言都會有的一種機制,是指同一個字串在記憶體中只會存放一份。同樣的在 .NET Common Language Runtime 裡,System.String 也是有 intern pool 的機制,讓同一個字串 (literal string) 只會保存在同一個記憶體區塊中,以提昇記憶體的使用效率。
string name1 = "Peter";
string name2 = "Peter";
Console.WriteLine("Reference of name1 & name2 are same: {0}",
Object.ReferenceEquals(name1, name2).ToString());
// Output: Reference of name1 & name2 are same: True
string name3 = "P";
name3 += "eter";
Console.WriteLine("Reference of name1 & name3 are same: {0}",
Object.ReferenceEquals(name1, name3).ToString());
// Output: Reference of name1 & name3 are same: False
首先宣告兩個字串變數並賦於相同的值,比較兩個變數的 ReferenceEquals 得到的結果是 True,表示兩個變數都指向同一個記憶體位置。
接下來宣告字串變數 name3 時我們用了字串相加,雖然其值都是一樣的,但比較 ReferenceEquals 的結果是 False,表示其記憶體位置不同。
Char[] chars = { 'P', 'e', 't', 'e', 'r' };
string name4 = new string(chars);
Console.WriteLine("Reference of name1 & name4 are same: {0}",
Object.ReferenceEquals(name1, name4).ToString());
// Output: Reference of name1 & name4 are same: False
其實在 CLR 裡,只有 compile-time 時是 literal string 的字串才會在 string pool 上建立。以上面的範例,在建立字串變數 name4 時是傳入一個 char 陣列,雖然其值也是 Peter,但比較 ReferenceEquals 的結果也是 False,所以不是每一個 "Peter" 都會存放同一個記憶體位置。
那如果字串是透過外部輸入,例如在 Console 是透過 ReadLine 或透過 TextBox 取值呢?
Console.Write(@"Please enter ""Peter"": ");
string name5 = Console.ReadLine();
Console.WriteLine("Reference of name1 & name5 are same: {0}",
Object.ReferenceEquals(name1, name5).ToString());
// Output: Reference of name1 & name5 are same: False
以上範例中字串變數 name5 的值是透過 Console.ReadLine 輸入,就算你輸入一模一樣的值,其 ReferenceEquals 的結果一樣是 False,因為在 compile-time 時並不是 literal string。
但如果我們希望就算是非 literal string 也可在 intern pool 裡指向同一個記憶體位置呢?在 .NET 裡 String 提供了一個 Intern 的靜態函式,會檢查 intern pool 裡是否己有該字串存在,如有則回傳同一個字串,其記憶體位置也會是一致的。
Console.Write(@"Please enter ""Peter"": ");
string name6 = String.Intern(Console.ReadLine());
Console.WriteLine("Reference of name1 & name6 are same: {0}",
Object.ReferenceEquals(name1, name6).ToString());
// Output: Reference of name1 & name7 are same: True
就以上範例,字串變數 name6 透過 Console.ReadLine 取後再呼叫 String.Intern 這個函式取得 intern pool 上的同一個字串,這時比較兩個變數的 ReferenceEquals 就會是 True 了,表示是同一個記憶體位置。
在 MSDN 上針對 string 的 intern pool 也有解釋,可以參考連結:
The common language runtime conserves string storage by maintaining a table, called the intern pool, that contains a single reference to each unique literal string declared or created programmatically in your program. Consequently, an instance of a literal string with a particular value only exists once in the system.
另外,在微軟公開 .NET Framework 的 source code 裡,我們看到 String.Intern 這個靜態函式的原始程式碼如下:
public static String Intern(String str) {
if (str==null) {
throw new ArgumentNullException("str");
}
Contract.Ensures(Contract.Result<string>().Length == str.Length);
Contract.Ensures(str.Equals(Contract.Result<string>()));
Contract.EndContractBlock();
return Thread.GetDomain().GetOrInternString(str);
}
我們看到,最後是透過 Thread.GetDomain 這個函式取回該執行緒的 AppDomain 物件,再呼叫 GetOrInternString 函式來取回 intern pool 裡的字串。
String 物件是不能改變的 (Immutable)
我們知道在 CLR 裡有 intern pool 的機制,讓同一字串只會保存一份。這種機制帶來了 System.String 的第二個特性,就是 String 物件是 immutable 的。在這裡 immutable 不可改變表示在 String 物件一經建立後,該記憶體位置的值則不能被改變,如果改變了該 string 物件,CLR 則會配置另一塊記憶體位置存放,原記憶體位置則交由 Garbage Collector 負責回收。
string name1 = "Peter";
string name2 = "Peter";
Console.WriteLine("name1 = {0}", name1);
Console.WriteLine("name2 = {0}", name2);
Console.WriteLine("name1 & name2 are same reference: {0}",
Object.ReferenceEquals(name1, name2).ToString());
// Output: name1 & name2 are same reference: True
看看以上範例,字串變數 name1 及 name2 都宣告並賦於同一個字串值,一經建立後 CLR 就依據 intern pool 的機制存放在同一個記憶體位置,比較兩個字串變數的 ReferenceEquals,其結果也是 True。
name2 = name2.Remove(name2.Length - 1, 1);
name2 += "r";
Console.WriteLine("name1 = {0}", name1);
Console.WriteLine("name2 = {0}", name2);
Console.WriteLine("name1 & name2 are same reference: {0}",
Object.ReferenceEquals(name1, name2).ToString());
// Output: name1 & name2 are same reference: False
接下來改變字串變數 name2 的值,先透過 Remove 函式去掉最後一個字元然後再加回去,雖然最終的值都一樣還是 "Peter",但其記憶體位置已經與字串變數 name1 不一樣了。比較兩個字串變數的 ReferenceEquals,其結果是 False 了。
其實依 intern pool 的機制來看,一開始字串變數 name1 及 name2 都指向同一個記憶體位置,如果變數 name2 要改變其值,CLR 必須要配置另外一塊記憶體區塊,要不然變數 name1 也會受到影響。這也解釋了為何許多書藉或文章都建議,如果針對 String 進行大量的串接 (concatenate) 時請使用 StringBuilder。
~Keeping Coding; Keep Writing
0 意見: