C#中的字符串不變性




.net pointers (5)

我很好奇StringBuilder類是如何在內部實現的,所以我決定查看Mono的源代碼並將其與Reflector的反彙編代碼進行比較。 從本質上講,Microsoft的實現使用char[]在內部存儲字符串表示,並使用一堆不安全的方法來操作它。 這很簡單,沒有提出任何問題。 但當我發現Mono在StringBuilder中使用一個字符串時,我很困惑:

private int _length;
private string _str;

第一個想法是:“多麼無謂的StringBuilder”。 但後來我發現可以使用指針改變字符串:

public StringBuilder Append (string value) 
{
     // ...
     String.CharCopy (_str, _length, value, 0, value.Length);
}

internal static unsafe void CharCopy (char *dest, char *src, int count) 
{
    // ...
    ((short*)dest) [0] = ((short*)src) [0]; dest++; src++;
}    

我曾經在C / C ++中編程一點,所以我不能說這段代碼讓我很困惑,但我認為字符串是完全不可變的(即絕對沒有辦法改變它)。 所以實際的問題是:

  • 我可以創建一個完全不可變的類型嗎?
  • 除性能問題外,是否有任何理由使用此類代碼? (更改不可變類型的不安全代碼)
  • 字符串本質上是線程安全的嗎?

我可以創建一個完全不可變的類型嗎?

是。 有一個構造函數來設置私有字段,只獲取屬性,沒有方法。

除性能問題外,是否有任何理由使用此類代碼?

一個例子:這樣的類型不需要從多個並發線程安全地使用鎖,這使得正確的代碼更容易編寫(沒有鎖定出錯)。

附加:足夠特權的代碼總是可以繞過.NET保護:要么反射讀取和寫入私有字段,要么是不安全的代碼來直接操作對象的內存。

這在.NET之外是正確的,一個特權進程(即具有一個“上帝”特權的進程或線程令牌,例如啟用Take Ownership)可以進入任何其他進程加載dll,注入運行任意代碼的線程,讀取或寫內存(包括覆蓋執行預防等)。 系統的完整性與系統所有者的合作一樣強大。


沒有完全不可變的類型,一個不可變的類是因為它不允許任何外部代碼改變它。 使用反射或不安全的代碼,您仍然可以更改它的值。

您可以使用readonly關鍵字創建不可變變量,但這僅適用於值類型。 如果在引用類型上使用它,它只是受保護的引用,而不是它指向的對象。

不可變類型有幾個原因,如性能和健壯性。

知道字符串是不可變的(在StringBuilder之外)意味著編譯器可以根據它進行優化。 編譯器永遠不必生成複製字符串的代碼,以防止它在作為參數傳遞時被更改。

從不可變類型創建的對像也可以在線程之間安全地傳遞。 由於它們無法更改,因此不同的線程不會同時更改它們,因此無需同步訪問它們。

不可變類型可用於避免編碼錯誤。 如果您知道不應更改某個值,通常最好確保不會錯誤地更改它。


如果你不安全,也可以在C#中改變字符串(IIRC)。


這裡沒有黑魔法。 字符串類是不可變的,因為它沒有任何允許您修改內部字符串的公共字段,屬性或方法。 任何改變字符串的方法都會返回一個新的字符串實例。 你當然也可以用你自己的課程來做這件事。


我可以創建一個完全不可變的類型嗎?

您可以創建CLR在其上強制實現不變性的類型。 然後,您可以使用“unsafe”來關閉CLR強制機制 。 這就是為什麼“不安全”被稱為“不安全” - 因為它關閉了安全系統。 在不安全的代碼中,如果你足夠努力, 包括不可變字節和CLR中強制不變性的代碼,那麼進程中每個字節的內存都是可寫的。

您還可以使用Reflection來打破不變性。 反射和不安全代碼都需要授予極高的信任度。

除性能問題外,是否有任何理由使用此類代碼?

當然,有很多理由使用不可變數據結構。 不可變數據結構搖滾 。 使用不可變數據結構的一些好理由:

  • 不可變數據結構比可變數據結構更容易推理。 當你問“這個清單是空的嗎?” 然後你會得到答案,你知道答案不僅僅是現在,而是永遠。 使用可變數據結構,您實際上無法問“這個列表是空的嗎?” 所有你能問的是“這個清單現在是空的嗎?” 然後答案在邏輯上回答了問題“這個列表在過去的某個時刻是空的嗎?”

關於不可變類型的問題的答案永遠保持為真的事實具有安全隱患。 假設你有這樣的代碼:

void Frob(Bar bar)
{
    if (!IsSafe(bar)) throw something;
    DoSomethingDangerous(bar);
}

如果Bar是一個可變類型,那麼這裡就存在競爭條件; 檢查後但發生危險之前,可能會在另一個線程上使條形圖不安全。 如果Bar是一個不可變類型,那麼問題的答案始終保持不變,這樣更安全。 (想像一下,如果你可以在安全檢查之後但文件打開之前改變包含路徑的字符串,例如。)

  • 將不可變數據結構作為參數並將其作為結果返回並且不執行副作用的方法稱為“純方法”。 可以記憶純方法,這可以增加內存使用以提高速度,通常可以極大地提高速度。

  • 不可變數據結構通常可以在不鎖定的情況下同時在多個線程上使用。 鎖定是為了防止在突變面前創建對象的不一致狀態,但是不可變對像沒有突變。 (一些所謂的不可變數據結構在邏輯上是不可變的,但實際上是在它們內部進行突變;想像一下例如一個查找表,它不會改變它的內容,但如果可以推斷出下一個查詢可能是什麼,它會重新組織它的內部結構。這樣的數據結構不會自動線程安全。)

  • 當從舊的結構構建新結構時,有效地重複使用其內部部件的不可變數據結構使得在不浪費大量內存的情況下“快照”程序狀態變得容易。 這使得undo-redo操作無法實現。 它使編寫調試工具變得更容易,可以向您展示如何進入特定的程序狀態。

  • 等等。

字符串本質上是線程安全的嗎?

如果每個人都遵守規則,他們就是。 如果有人使用不安全的代碼或私人反射,則不再執行規則 。 你必須相信,如果有人使用高權限代碼,那麼他們正在這樣做,而不是改變字符串。 用你的力量只運行不安全的代碼; 擁有權利的同時也被賦予了重大的責任。

那麼我需要使用鎖嗎?

這是一個奇怪的問題。 請記住,鎖是合作的 。 只有在訪問特定對象的每個人都同意必須使用的鎖定策略時,鎖才有效。

如果用於訪問特定存儲位置中的特定對象的約定鎖定策略是使用鎖,則必須使用鎖。 如果這不是商定的鎖定策略那麼使用鎖是沒有意義的; 當其他人在敞開的後門走動時,你小心地鎖定和解鎖前門。

如果你有一個你知道被不安全代碼變異的字符串,並且你不希望看到不一致的部分突變,並且正在執行不安全突變文件的代碼在該突變期間取出特定的鎖,那麼是,你需要在訪問該字符串時使用鎖。 但這種情況非常罕見; 理想情況下,沒有人會使用不安全的代碼來操縱另一個線程上其他代碼可訪問的字符串,因為這樣做是一個非常糟糕的主意。 這就是為什麼我們要求完全信任的代碼。 這就是為什麼我們要求這樣一個函數的C#源代碼發出一個大紅旗,上面寫著“這段代碼不安全,請仔細檢查!”





immutability