繁体   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