为什么C#中的结构体是不可变的?

67

我只是好奇为什么结构体、字符串等是不可变的?为什么要把它们设为不可变,而其他对象则为可变。哪些方面被认为是使对象不可变的原因?

对于可变和不可变对象,内存的分配和释放方式是否有区别?


18
结构体本身并非不可变的,通常在使用结构体时会将其设计为不可变,但这不是其本质属性。 - Marc
2
此SO帖子应该包含您所需的所有内容。 - cjk
并非所有的引用类型都是可变的。实际上,有些可能根本没有任何状态(例如实现特定接口的策略类,如 IComparer<T>)。 - Dan Bryant
5个回答

133
如果您对此话题感兴趣,我有一系列有关不可变编程的文章,请访问https://ericlippert.com/2011/05/26/atomicity-volatility-and-immutability-are-different-part-one/阅读。
“为什么structs、strings等是不可变的?” 结构体和类默认情况下并非不可变的,但将结构体定义为不可变是最佳实践。我也喜欢不可变类。 字符串是不可变的。
“为什么要将它们设为不可变,而其他对象则为可变?” 使所有类型都不可变的原因: - 更容易理解不会发生更改的对象。如果我有一个包含三个项的队列,我知道它现在不是为空的,五分钟前也不是空的,未来也不会是空的。它是不可变的!一旦我知道了它的状态,我可以永久地使用这个状态。关于不可变对象的特定事实不会过时。 - 第一个点的特例:不可变对象更容易实现线程安全。大多数线程安全问题是由于在一个线程上进行写操作,在另一个线程上进行读操作;不可变对象没有写操作。 - 不可变对象可以被拆分和重复使用。例如,如果您有一个不可变的二叉树,那么您可以使用其左子树和右子树作为不同树的子树,而不必担心它。在可变的结构中,您通常会复制数据以重新使用它,因为您不希望更改一个逻辑对象影响另一个对象。这可以节省大量时间和内存。
将结构体设为不可变的原因: - 有很多将结构体定义为不可变的理由。以下是其中之一。 - 结构体是按值复制的,而不是按引用复制的。很容易错误地将结构体视为按引用复制的。例如:
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)不会改变等等。

有时类型表示会变化的事物。玛丽·史密斯的姓氏是史密斯,但明天她可能成为玛丽·琼斯。或者今天的史密斯小姐可能明天成为史密斯博士。外星人现在有五十个生命值,但被激光束击中后只剩下十个。某些事物最好表示为突变。

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

并没有这样的区别。但正如我之前提到的,不可变值的一个好处是,您可以重复使用它们的部分而无需进行复制。因此,在这种意义上,内存分配可能会非常不同。


2
我认为他们所说的十分清晰明了...感谢如此详细的解释... - Sandeep
4
假设你有一摞书。如果你想在它们的顶部放置另一本书,那么将这本书放在它们上面要比购买每本书的副本、按相同顺序堆叠它们,然后再将新书放在它们上面更自然。像集合这样的对象通常被期望进行更改,因此将其视为不可变的东西并不直观。 - Gigi
7
@gigi 不要购买每本书的第二份副本。相反,当您添加第三本书时,您现在有四个栈。空栈,顶部放着一本书的空栈,顶部放着一本书的另一个栈,以及顶部放着一本书的另一个栈。恰恰是因为您不想购买更多的书才使得不可变性生效!需要复制的是可变数据结构。你完全颠倒了。 - Eric Lippert
2
@EricLippert 我现在明白了。谢谢,你总是一个很棒的老师。 - Gigi
2
@NDUF:让我换一种方式表达。假设您有堆栈ABC,并且您想同时获取ABCX和ABCY两个堆栈。如果堆栈ABC是可变的,则必须复制,将X推送到原始堆栈并将Y推送到副本。如果堆栈ABC是不可变的,则可以在创建两个新堆栈时只复制对ABC的引用。您不必复制整个堆栈数据结构。具有此属性的结构称为“持久性”。 - Eric Lippert
显示剩余9条评论

9

结构体并不一定是不可变的,但是可变的结构体是有害的。

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

另一方面,字符串是不可变的。这使得它们天生是线程安全的,并且可以通过字符串内部化进行优化。如果需要动态构建复杂的字符串,则可以使用StringBuilder


他可能是指为什么BCL中的结构体和字符串是不可变的(一般而言,尽管我不知道有任何可变的结构体),而不是为什么所有结构体都是不可变的(你的答案正确地纠正了这一点)。 - Anthony Pegram
@Anthony:BCL中有很多IEnumerator实现是结构体。例如http://msdn.microsoft.com/en-us/library/x854yt9s.aspx不要通过属性公开其中之一,否则会遭受可怕的后果。 :) - Joren

5
当应用于结构体和类时,可变性和不可变性的概念有不同的含义。 可变类的一个关键方面(通常是其主要缺陷)是如果Foo具有类型为List<Integer>的字段Bar,它持有对包含(1,2,3)的列表的引用,则具有对该同一列表的引用的其他代码可以修改它,使得Bar持有对包含(4,5,6)的列表的引用,即使那个其他代码根本没有访问Bar的权限。相比之下,如果Foo具有类型为System.Drawing.Point的字段Biz,任何修改Biz的方面的方式都必须具有对该字段的写入访问权限
一个结构体的公共和私有字段可以被任何能够改变存储位置的代码改变,而不能被任何不能改变其存储位置的代码改变。如果一个结构体封装的所有信息都保存在其字段中,这样的结构体可以有效地将不可变类型的控制与可变类型的便利性相结合,除非结构体被编码为删除此类便利性(不幸的是,一些微软程序员推荐这种习惯)。
结构体的“问题”在于,在只读上下文(或不可变位置)中调用结构体的方法(包括属性实现)时,系统会复制结构体,在临时副本上执行该方法,并默默地丢弃结果。这种行为导致程序员提出了一个不幸的观点,即避免变异方法的问题的方法是让许多结构体不允许逐个更新,而问题本来可以通过仅使用公开字段替换属性更好地避免。
顺便说一下,有些人抱怨当类属性返回一个方便可变的结构体时,对结构体的更改不会影响它来自的类。我认为这是件好事——返回的项目是一个结构体,这使得行为清晰明了(特别是如果它是一个公开字段的结构体)。将使用假设的结构体和属性在Drawing.Matrix上与Microsoft实现的该类上使用实际属性的代码片段进行比较:
// 假设的结构体 public struct { public float xx,xy,yx,yy,dx,dy; } Transform2d;
// "System.Drawing.Drawing2d.Matrix"的假设属性 public Transform2d Transform {get;}
// "System.Drawing.Drawing2d.Matrix"的实际属性 public float[] Elements { get; }
// 使用假设的结构体的代码 Transform2d myTransform = myMatrix.Transform; myTransform.dx += 20; ... 使用myTransform的其他代码
// 使用实际Microsoft属性的代码 float[] myArray = myMatrix.Elements; myArray[4] += 20; ... 使用myArray的其他代码

看着实际的 Microsoft 属性,有没有办法判断对 myArray[4] 的写入是否会影响 myMatrix?即使查看页面 http://msdn.microsoft.com/en-us/library/system.drawing.drawing2d.matrix.elements.aspx 也无法确定吗?如果使用基于结构体的等效属性编写该属性,则不会存在任何混淆;返回结构体的属性将仅返回六个数字的当前值。更改 myTransform.dx 将只是对未连接到任何其他内容的浮点变量进行写入。任何不喜欢更改 myTransform.dx 不会影响 myMatrix 的人应该同样感到烦恼,因为除了明显的 myMatrixmyTransform 的独立性外,写入 myArray[4] 也不会影响 myMatrix,而 myMatrixmyArray 的独立性则不是这样。


4

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


1

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

可变结构体会导致错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接