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

时间:2010-09-20 13:28:53

标签: c# .net immutability

我只是想知道为什么结构,字符串等是不可变的?是什么原因使它们成为不可变的,其余的对象是可变的。有什么东西被认为是使对象不可变的?

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

5 个答案:

答案 0 :(得分:113)

如果这个主题让你感兴趣,我在http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/

上有很多关于不可变编程的文章
  

我只是想知道为什么结构,字符串等是不可变的?

默认情况下,结构和类不是不可变的,尽管使结构不可变是最佳实践。我也喜欢不可变的课程。

字符串是不可变的。

  

使它们不可变的原因是什么,其余的对象是可变的。

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

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

  • 第一点的特例:不可变对象更容易使线程安全。大多数线程安全问题是由于在一个线程上写入而在另一个线程上读取;不可变对象没有写入。

  • 可以拆开和重复使用不可变对象。例如,如果你有一个不可变的二叉树,那么你可以使用它的左右子树作为不同树的子树而不用担心它。在可变结构中,您通常最终会复制数据以重复使用它,因为您不希望更改一个逻辑对象而影响另一个逻辑对象。这可以节省批次的时间和内存。

使结构不可变的原因

有很多理由让结构不可变。这只是一个。

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

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)没有变化。等等。

有时候,这种类型代表了改变的事物。玛丽史密斯的姓是史密斯,但明天她可能是玛丽琼斯。或史密斯小姐今天可能是明天的史密斯医生。外星人现在有五十个健康点,但在被激光束击中后有十点。有些事情最能代表突变。

  

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

不是这样的。正如我之前提到的,关于不可变值的一个好处是,你可以重复使用它们的一部分,而不需要复制。所以从这个意义上说,内存分配可能会有很大不同。

答案 1 :(得分:9)

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

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

另一方面,字符串是。这使它们本身具有线程安全性,并允许通过字符串实习进行优化。如果您需要动态构建复杂的字符串,可以使用StringBuilder

答案 2 :(得分:4)

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

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

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

顺便提一下,有些人抱怨当一个类属性返回一个方便可变的结构时,对结构的更改不会影响它来自的类。我认为这是一件好事 - 事实上返回的项是一个结构使得行为清晰(特别是如果它是一个暴露的字段结构)。比较使用myArray[4]上的假设结构和属性的片段与使用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属性,有没有办法判断对myMatrix的写入是否会影响myTransform.dx?即使查看页面http://msdn.microsoft.com/en-us/library/system.drawing.drawing2d.matrix.elements.aspx,还有什么方法可以说明吗?如果该属性是使用基于结构的等价物编写的,则不会产生混淆;返回结构的属性将返回不多于或少于六个数字的当前值。改变myTransform.dx只不过是对浮点变量的写入,而浮点变量与其他任何东西都没有关系。任何不喜欢更改myMatrix不会影响myArray[4]这一事实的人应该同样恼火,因为写myMatrix也不会影响myMatrix,除了独立myTransformmyMatrix的显而易见,而myArray和{{1}}的独立性则不然。

答案 3 :(得分:2)

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

答案 4 :(得分:1)

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

可变结构会导致错误。