关于.net:为什么C#结构是不可变的?

Why are C# structs immutable?

我只是好奇为什么结构、字符串等是不变的?是什么原因使它们不可变,而其他对象则是可变的?什么东西被认为是使一个物体不可变的?

对于可变和不可变对象,如何分配和释放内存有什么不同吗?


如果你对这个主题感兴趣,我在http://blogs.msdn.com/b/ericlippet/archive/tags/immutatability上有很多关于不可变编程的文章。/

I was just curious to know why structs, strings etc are immutable?

默认情况下,结构和类不是不可变的,但最好是使结构不可变。我也喜欢不变的类。

字符串是不可变的。

What is the reason for making them immutable and rest of the objects as mutable.

使所有类型不可变的原因:

  • 对于不发生更改的对象,更容易进行推理。如果我有一个包含三个项目的队列,我知道它现在不是空的,五分钟前不是空的,将来也不会是空的。它是不变的!一旦我知道了一个事实,我就可以永远使用这个事实。关于不可变对象的事实不会过时。

  • 第一点的一个特殊情况是:不可变对象更容易生成线程安全。大多数线程安全问题是由一个线程上的写操作和另一个线程上的读操作引起的;不可变对象没有写操作。

  • 不可变的对象可以分离并重新使用。例如,如果您有一个不可变的二叉树,那么您可以使用它的左子树和右子树作为不同树的子树,而不用担心它。在一个可变结构中,您通常会制作数据的副本来重用它,因为您不希望对一个逻辑对象所做的更改影响到另一个逻辑对象。这样可以节省大量时间和内存。

使结构不可变的原因

使结构不可变有很多原因。这里只有一个。

结构是按值而不是按引用复制的。很容易意外地将结构视为被引用复制。例如:

1
2
3
4
5
6
7
8
9
void M()
{
    S s = whatever;
    ... lots of code ...
    s.Mutate();
    ... lots more code ...
    Console.WriteLine(s.Foo);
    ...
}

现在,您要将其中的一些代码重构为一个助手方法:

1
2
3
4
5
6
void Helper(S s)
{
    ... lots of code ...
    s.Mutate();
    ... lots more code ...
}

错了!这应该是(参考S)--如果你不这样做,突变就会发生在一个S的拷贝上。如果你不允许突变,那么所有这些问题都会消失。

使字符串不可变的原因

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

假设字符串是可变的:

1
2
3
4
5
public static File OpenFile(string filename)
{
    if (!HasPermission(filename)) throw new SecurityException();
    return InternalOpenFile(filename);
}

如果敌方调用者在安全检查之后和打开文件之前更改了文件名,该怎么办?代码刚刚打开了一个他们可能没有权限的文件!

同样,易变数据很难解释。您希望"这个调用者有权看到这个字符串描述的文件"永远是真的,直到发生变异。对于可变字符串,要编写安全代码,我们必须不断地复制我们知道不会改变的数据。

What are the things that are considered to make an object immutable?

类型是否在逻辑上表示"永恒"值?数字12是数字12,它不变。整数应该是不可变的。点(10,30)是点(10,30);它不会改变。点应该是不可变的。字符串"abc"是字符串"abc";它不会改变。字符串应该是不可变的。列表(10,20,30)不变。等等。

有时,类型表示确实发生变化的事物。玛丽·史密斯的姓史密斯,但明天她可能是玛丽·琼斯。或者史密斯小姐今天可能是史密斯医生明天。外星人现在有50个健康点,但被激光束击中后有10个。有些东西最好用突变来表示。

Is there any difference on the way how memory is allocated and deallocated for mutable and immutable objects?

不是这样的。不过,正如我之前提到的,关于不变值的一个好处是,可以在不复制的情况下重用其中的一部分。所以从这个意义上说,内存分配可能是非常不同的。


结构不是…这就是为什么可变结构是邪恶的。

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

另一方面,弦是。这使得它们在本质上是线程安全的,并且允许通过字符串内部进行优化。如果需要动态构造复杂的字符串,可以使用StringBuilder


可变性和不可变性的概念在应用于结构和类时具有不同的含义。可变类的一个关键方面(通常,关键弱点)是,如果Foo具有List类型的字段Bar,该字段保存对包含(1,2,3)的列表的引用,其他引用该列表的代码也可以修改该列表,这样,Bar保存对包含(4,5,6)的列表的引用,即使其他代码无法访问Bar。相反,如果Foo有一个System.Drawing.Point类型的字段Biz,任何修改Biz的任何方面的唯一方法就是拥有对该字段的写访问权。

结构的字段(public和private)可以由任何可以改变存储结构的存储位置的代码来改变,也不能由任何不能改变存储结构的存储位置的代码来改变。如果结构中封装的所有信息都保存在其字段中,那么这样的结构可以有效地将对不可变类型的控制与可变类型的方便结合起来,除非结构的编码方式消除了这种方便(不幸的是,一些Microsoft程序员建议这种习惯)。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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

从微软的实际资产来看,有没有办法判断写入myArray[4]是否会影响myMatrix?即使在http://msdn.microsoft.com/en-us/library/system.drawing2d.matrix.elements.aspx页面上也能看出什么?如果该属性是使用基于结构的等价物编写的,则不会出现混淆;返回该结构的属性返回的值不会大于或小于6个数字的当前值。更改myTransform.dx只不过是对一个不与任何其他变量相连的浮点变量进行写入。任何人如果不喜欢改变myTransform.dx不影响myMatrix的事实,那么写myArray[4]也不影响myMatrix,除了myMatrixmyTransform的独立性是明显的,而myMatrixmyArray的独立性是不明显的。


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


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

可变结构会导致错误。