【问题标题】:Why are C# structs immutable?为什么 C# 结构是不可变的?
【发布时间】:2010-09-20 13:28:53
【问题描述】:

我只是想知道为什么结构、字符串等是不可变的?使它们不可变而其余对象可变的原因是什么。什么东西可以让一个对象不可变?

可变和不可变对象的内存分配和释放方式有什么不同吗?

【问题讨论】:

  • 结构并非天生不可变,不过,这通常是使用它们时的设计目标。
  • This SO post 应该有你需要的一切。
  • 并非所有引用类型都是可变的。事实上,有些可能没有任何状态(例如实现特定接口的策略类,如IComparer<T>)。

标签: c# .net immutability


【解决方案1】:

如果你对这个主题感兴趣,我有很多关于不可变编程的文章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 的 copy 上。如果你一开始就不允许突变,那么所有这些问题都会消失。

使字符串不可变的原因

还记得我关于不可变结构保持事实的第一点吗?

假设字符串是可变的:

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 点。有些东西最好用突变来表示。

可变和不可变对象的内存分配和释放方式有什么不同吗?

并非如此。正如我之前提到的,关于不可变值的好处之一是您可以重复使用它们的一部分而无需复制。所以从这个意义上说,内存分配可能非常不同。

【讨论】:

  • 我认为这就是他们所说的 Crystal Clear...感谢您提供如此详细的解释...
  • @EricLippert 假设你有一摞书。如果你想在上面放另一本书,那么把这本书放在上面要自然得多,而不是买每本书的副本,按相同的顺序堆叠,然后再放置新书在那些之上。诸如集合之类的对象通常会发生变化,将它们推断为不可变的东西并不直观。
  • @gigi 你不要为每本书买第二本。相反,当您添加第三本书时,您现在有四个堆栈。空一摞,一摞放在空摞上的摞,一摞摞摞摞上,摞摞摞一摞摞。正是因为你不想想买更多的书,所以不变性才起作用!它是需要复制的可变数据结构。你完全倒退了。
  • @EricLippert 我现在看到了曙光。谢谢,您总是一位出色的老师。
  • @NDUF:让我换一种说法。假设您有堆栈 ABC,并且您想要 both 堆栈 ABCX 和 ABCY。如果堆栈 ABC 是可变,那么你必须制作一个副本,将 X 推到原件上,将 Y 推到副本上。如果堆栈 ABC 是不可变的,那么您可以在创建两个新堆栈时将 reference 复制到 ABC。您不必复制整个堆栈数据结构。具有此属性的结构称为“持久”。
【解决方案2】:

结构不一定是不可变的,但可变结构是邪恶的。

创建可变结构可能会导致应用程序中出现各种奇怪的行为,因此,它们被认为是一个非常糟糕的主意(因为它们看起来像引用类型但实际上是值类型并且会被复制每当您传递它们时)。

另一方面,字符串是不可变的。这使得它们本质上是线程安全的,并允许通过字符串实习进行优化。如果你需要即时构造一个复杂的字符串,你可以使用StringBuilder

【讨论】:

  • 他的意思可能是为什么 BCL 中的结构和字符串是不可变的(一般来说,虽然我不知道任何可变结构),而不是为什么 all 结构是不可变的(你的回答正确,嗯,更正)。
  • @Anthony:BCL 中的很多IEnumerator 实现都是结构体。例如。 msdn.microsoft.com/en-us/library/x854yt9s.aspx不要通过财产暴露其中一个,否则会遭受可怕的后果。 :)
【解决方案3】:

可变性和不变性的概念在应用于结构和类时具有不同的含义。可变类的一个关键方面(通常是关键弱点)是如果Foo 有一个Bar 类型为List<Integer> 的字段,它包含对包含(1,2,3) 的列表的引用,其他代码具有对同一个列表的引用可以修改它,使得Bar 持有对包含 (4,5,6) 的列表的引用,即使其他代码无法访问 Bar .相比之下,如果Foo 有一个Biz 类型为System.Drawing.Point 的字段,那么任何可以修改Biz 任何方面的唯一方法就是拥有对该字段的写入权限。 p>

结构的字段(公共和私有)可以被任何可以改变存储结构的存储位置的代码改变,并且不能被任何不能改变存储它的存储位置的代码改变。如果封装在结构中的所有信息都保存在其字段中,则这种结构可以有效地将不可变类型的控制与可变类型的便利性结合起来,除非结构的编码方式消除了这种便利性(不幸的是,一些微软程序员推荐的习惯)。

结构体的“问题”是,当在只读上下文(或不可变位置)中的结构体上调用方法(包括属性实现)时,系统会复制该结构体,并在临时副本上执行该方法,并默默地丢弃结果。这种行为导致程序员提出了一个不幸的概念,即避免变异方法问题的方法是让许多结构不允许分段更新,而只需用暴露的字段替换属性就可以更好地避免这些问题>.

顺便说一句,有些人抱怨当类属性返回一个方便可变的结构时,对结构的更改不会影响它来自的类。我认为这是一件好事——返回的项目是一个结构这一事实使行为变得清晰(特别是如果它是一个暴露字段结构)。将使用Drawing.Matrix 上的假设结构和属性的 sn-p 与使用 Microsoft 实现的该类上的实际属性的 sn-p 进行比较:

// 假设结构 公共结构{ 公共浮动 xx,xy,yx,yy,dx,dy; } 变换2d; // “System.Drawing.Drawing2d.Matrix”的假设属性 公共 Transform2d 变换 {get;} // “System.Drawing.Drawing2d.Matrix”的实际属性 公共浮动 [] 元素 { 获取; } // 使用假设结构的代码 Transform2d myTransform = myMatrix.Transform; myTransform.dx += 20; ... 使用 myTransform 的其他代码 // 使用实际 Microsoft 属性的代码 浮动[] myArray = myMatrix.Elements; 我的数组 [4] += 20; ... 使用 myArray 的其他代码

看看实际的微软属性,有没有办法判断写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 的独立性则不是。

【讨论】:

    【解决方案4】:

    结构类型不是不可变的。是的,字符串是。使您自己的类型不可变很容易,只需不提供默认构造函数,将所有字段设为私有,并且不定义更改字段值的方法或属性。有一个应该改变对象的方法返回一个新对象。有一个内存管理的角度,你往往会创建大量的副本和垃圾。

    【讨论】:

      【解决方案5】:

      结构可以是可变的,但这是一个坏主意,因为它们具有复制语义。如果您对结构进行更改,您实际上可能正在修改副本。准确跟踪已更改的内容非常棘手。

      可变结构滋生错误。

      【讨论】:

        猜你喜欢
        • 2010-10-01
        • 1970-01-01
        • 1970-01-01
        • 2022-11-26
        • 1970-01-01
        • 2015-09-27
        • 1970-01-01
        相关资源
        最近更新 更多