簡體   English   中英

為什么 C# 結構是不可變的?

[英]Why are C# structs immutable?

我只是想知道為什么結構、字符串等是不可變的? 使它們不可變而其余對象可變的原因是什么。 哪些東西被認為是使對象不可變的?

為可變和不可變對象分配和釋放內存的方式有什么不同嗎?

如果您對這個主題感興趣,我在https://ericlippert.com/2011/05/26/atomicity-volatility-and-immutability-are-different-part-one/ 上有許多關於不可變編程的文章

我只是想知道為什么結構、字符串等是不可變的?

默認情況下,結構和類不是不可變的,盡管使結構不可變是最佳實踐。 我也喜歡不可變的類。

字符串是不可變的。

使它們不可變而其余對象可變的原因是什么。

使所有類型不可變的原因:

  • 對不變的對象進行推理更容易。 如果我有一個包含三個項目的隊列,我知道它現在不是空的,五分鍾前不是空的,將來也不會是空的。 它是不可變的! 一旦我知道了一個事實,我就可以永遠使用這個事實。 關於不可變對象的事實不會過時。

  • 第一點的特例:不可變對象更容易使線程安全。 大多數線程安全問題是由於在一個線程上寫入而在另一個線程上讀取; 不可變對象沒有寫入。

  • 不可變對象可以被拆開並重新使用。 例如,如果您有一個不可變的二叉樹,那么您可以將其左右子樹用作不同樹的子樹而無需擔心。 在可變結構中,您通常最終會制作數據副本以重新使用它,因為您不希望對一個邏輯對象的更改影響另一個。 這可以節省大量時間和內存。

使結構不可變的原因

使結構不可變的原因有很多。 這里只有一個。

結構是按值復制的,而不是按引用復制的。 很容易意外地將結構視為通過引用復制。 例如:

void M()
{
    S s = whatever;
    ... lots of code ...
    s.Mutate();
    ... lots more code ...
    Console.WriteLine(s.Foo);
    ...
}

現在您想將其中的一些代碼重構為一個輔助方法:

void Helper(S s)
{
    ... lots of code ...
    s.Mutate();
    ... lots more code ...
}

錯了! 那應該是 (ref S s)——如果你不這樣做,那么突變將發生在 s 的副本上。 如果您一開始就不允許突變,那么所有這些問題都會消失。

使字符串不可變的原因

還記得我關於不可變結構保持事實的第一點嗎?

假設字符串是可變的:

public static File OpenFile(string filename)
{
    if (!HasPermission(filename)) throw new SecurityException();
    return InternalOpenFile(filename);
}

如果安全檢查,文件之前敵對呼叫者變異名被打開怎么辦? 代碼只是打開了一個他們可能沒有權限的文件!

同樣,可變數據很難推理。 您希望“此調用者有權查看此字符串描述的文件”這一事實永遠成立直到發生突變 對於可變字符串,為了編寫安全代碼,我們必須不斷地制作我們知道不會改變的數據副本。

哪些東西被認為是使對象不可變的?

該類型是否在邏輯上代表了“永恆”值? 數字12就是數字12; 它不會改變。 整數應該是不可變的。 點 (10, 30) 是點 (10, 30); 它不會改變。 點應該是不可變的。 字符串“abc”就是字符串“abc”; 它不會改變。 字符串應該是不可變的。 列表 (10, 20, 30) 不會改變。 等等。

有時類型代表確實發生變化的事物。 瑪麗史密斯的姓是史密斯,但明天她可能會叫瑪麗瓊斯。 或者今天的史密斯小姐明天可能就是史密斯醫生。 外星人現在有 50 點生命值,但在被激光束擊中后有 10 點。 有些東西最好用突變來表示。

為可變和不可變對象分配和釋放內存的方式有什么不同嗎?

不是這樣。 不過,正如我之前提到的,不可變值的好處之一是您可以重用它們的一部分而無需復制。 所以從這個意義上說,內存分配可能會有很大不同。

結構不一定是不可變的,但可變結構是邪惡的。

創建可變結構可能會導致應用程序中出現各種奇怪的行為,因此,它們被認為是一個非常糟糕的主意(因為它們看起來像引用類型,但實際上是值類型,並且每當您通過時都會被復制)他們周圍)。

另一方面,字符串是不可變的。 這使得它們本質上是線程安全的,並允許通過字符串實習進行優化。 如果您需要即時構造復雜的字符串,可以使用StringBuilder

可變性和不變性的概念在應用於結構和類時具有不同的含義。 可變類的一個關鍵方面(通常是關鍵弱點)是如果Foo有一個List<Integer>類型的字段Bar ,它持有對包含 (1,2,3) 的列表的引用,其他代碼引用同一個列表可以修改它,這樣Bar持有對包含 (4,5,6) 的列表的引用,即使其他代碼無法訪問Bar 相比之下,如果Foo有一個System.Drawing.Point類型的字段Biz ,那么任何可以修改Biz任何方面的唯一方法就是對該字段具有寫訪問權限

結構體的字段(公共和私有)可以被任何可以改變結構體存儲位置的代碼改變,並且不能被任何不能改變結構體存儲位置的代碼改變。 如果結構體中封裝的所有信息都保存在其字段中,則這樣的結構體可以有效地將不可變類型的控制與可變類型的便利性結合起來,除非該結構體的編碼方式消除了這種便利性(不幸的是,一些微軟程序員推薦了一個習慣)。

結構體的“問題”在於,當在只讀上下文(或不可變位置)中的結構體上調用方法(包括屬性實現)時,系統會復制該結構體,在臨時副本上執行該方法,然后靜默丟棄結果。 這種行為導致程序員提出了一個不幸的想法,即避免變異方法問題的方法是讓許多結構禁止分段更新,而通過簡單地用公開的字段替換屬性可以更好地避免問題。

順便說一句,有些人抱怨當類屬性返回一個方便可變的結構時,對結構的更改不會影響它來自的類。 我認為這是一件好事——返回的項目是一個結構的事實使行為變得清晰(特別是如果它是一個暴露的字段結構)。 將使用Drawing.Matrix上的假設結構和屬性的片段與使用 Microsoft 實現的該類上的實際屬性的片段進行比較:

// Hypothetical struct
public struct {
  public float xx,xy,yx,yy,dx,dy;
} Transform2d;

// Hypothetical property of "System.Drawing.Drawing2d.Matrix"
public Transform2d Transform {get;}

// Actual property of "System.Drawing.Drawing2d.Matrix"
public float[] Elements { get; }

// Code using hypothetical struct
Transform2d myTransform = myMatrix.Transform;
myTransform.dx += 20;
... other code using myTransform

// Code using actual Microsoft property
float[] myArray = myMatrix.Elements;
myArray[4] += 20;
... other code using myArray

查看實際的 Microsoft 屬性,有什么方法可以判斷寫入myArray[4]是否會影響myMatrix 甚至看頁面http://msdn.microsoft.com/en-us/library/system.drawing.drawing2d.matrix.elements.aspx有什么辦法告訴嗎? 如果屬性是使用基於結構的等價物編寫的,就不會有混淆; 返回結構的屬性不會返回比六個數字的當前值更多或更少的值。 更改myTransform.dx只不過是寫入一個不附加任何其他內容的浮點變量。 任何不喜歡改變myTransform.dx不影響myMatrix應該同樣惱火,寫myArray[4]也不影響myMatrix ,除了myMatrixmyTransform的獨立性很明顯,而myMatrixmyArray不是。

結構類型不是一成不變的。 是的,字符串是。 使您自己的類型不可變很容易,只需不提供默認構造函數,將所有字段設為私有並且不定義更改字段值的方法或屬性。 有一個應該改變對象的方法返回一個新對象。 有一個內存管理角度,你往往會創建大量的副本和垃圾。

結構可以是可變的,但這是一個壞主意,因為它們具有復制語義。 如果對結構進行更改,則實際上可能是在修改副本。 准確跟蹤已更改的內容非常棘手。

可變結構會導致錯誤。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM