C# String的本質

2020-07-16 10:04:47
字串(string)實際上就是字元的集合(char[])。

字串型別是基元型別,它對應著 System.String。

字串的型別定義如下:

public sealed class String : IComparable, ICloneable, IConvertible,
        IEnumerable, IComparable<string>, IEnumerable<char>, IEquatable<string>

字串是一個型別,而不是結構,因此,字串範例當然是參照型別。

可以通過字串的預設值為 null 來記憶這點,對字串的操作伴隨著堆上的記憶體活動。

我們可以看到,字串繼承了IEnumerable<char>,這使得它可以使用 LINQ 查詢。

也許你會認為字串完全可以被設計為結構,原因如下:
  • 字串是密封類,而結構不能被繼承,所以沒問題。
  • 字串繼承了很多介面,而結構也可以繼承介面。
  • 字串的欄位和屬性很少而且都是值型別。
  • 字串的比較僅僅會比較值。
  • 字串做方法引數行為類似值型別。

但是,最終微軟將字串設計為一個密封類(而且具有不變性),這基於以下原因:
  • 字串的長度可能十分巨大,而一個執行緒棧只有 1MB 的空間。其他值型別的長度都是確定的。
  • 字串如果被設計為值型別,則方法傳入字串將會拷貝其值而不是參照。如果字串長度巨大,則效能會顯著下降。
  • 不變性使得字串是執行緒安全的。
  • 字串駐留節省記憶體,而它只可能借助不可變性來實現。

字串與普通的參照型別相比

字串的行為很像值型別:
  • 字串使用雙等於號互相比較時,比較的是字串的值而不是其是否指向同一個參照。這與型的比較不同,卻和值型別的比較相同。
  • 字串雖然是參照型別,但如果在某方法中,將字串傳入另一方法,在另一方法內部修改,執行完之後,字串的值並不會改變,而參照型別無論是按值傳遞還是參照傳遞,值都會發生變化。

對第一點來說,字串的==操作符被重寫為比較字串的值而不是其參照。

作為參照型別,==本來是比較參照的,但此時被重寫,這也是字串看起來像值型別的一個原因。當然,!=操作符也會一併被重寫。

[__DynamicallyInvokable]
public static bool operator ==(string a, string b)
{
    return string.Equals(a, b);
}
[__DynamicallyInvokable]
publie static bool operator !=(string a, string b)
{
    return !string.Equals(a, b);
}

IL中建立字串

在 C# 中,不能使用 new 操作符建立字串,但可以為字串直接賦值;也支援傳入一個字元集合。

通過 IL 分析,我們可以知道,如果為字串直接賦值,則建立字串的指令是 ldstr 如果傳入了一個字元集合,則指令是平常的 newobj。程式碼如下:
// string s = new string("1");
string s = "1";
string t = new string(new char[]{'1', '2', ' 3 ' });
對應的IL程式碼如下:

IL_0000:  nop
IL_0001:  ldstr      "1"
IL_0006:  stloc.0
IL_0007:  ldc.i4.3
IL_0008:  newarr     [mscorlib]System.Char
IL_000d:  dup
IL_000e:  ldtoken    field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=6' '<PrivateImplementationDetails>'::'0D5399508427CE79556CDA71918020C1E8D15B53'
IL_0013:  call       void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array,
                                                                                                    valuetype [mscorlib]System.RuntimeFieldHandle)
IL_0018:  newobj     instance void [mscorlib]System.String::.ctor(char[])

我們可以看到在 0007 和最後一行 IL 程式碼,分別使用了 ldstr 和 newobj 去建立字串。

ldstr 是 load string 的縮寫,用於獲得文字常數。這個操作需要在堆上分配空間,並將文字常數轉換為對應的 unicode 字元陣列。

而 newobj 則是常規的參照型別建立方式,其會呼叫 string 的建構函式。

字串的不變性

Immutability 般翻譯為不變性,也有翻譯為恆等性的。而相對的,泛型的協變和逆變也有不變性(invariant),它們對應的英文不同。

因此,應當在中文層面上將這兩個術語加以區分。不過,現在大部分人都將“一經賦值,值就不能被更改”這個現象翻譯為不變性。

字串的不變性指的是字串一經賦值,其值就不能被更改。當使用程式碼將字串變數等於一個新的值時,堆上會出現一個新的字串,然後,棧上的變數指向該新字串。

沒有任何辦法更改原來字串的值。如下圖所示。

字符串的不變性