不可变类vs结构

时间:2013-01-03 21:14:07

标签: c# class struct immutability

以下是类与C#中的结构不同的唯一方法(如果我错了请纠正我):

  • 类变量是引用,而struct变量是值,因此struct的整个值在赋值和参数传递中被复制
  • 类变量是存储在堆栈上的指针,指向堆上的内存,而struct变量作为值存储在堆上

假设我有一个不可变的结构,即struct,其字段在初始化后无法修改。每次我将此结构作为参数传递或在赋值中使用时,该值都将被复制并存储在堆栈中。

然后假设我使这个不可变的结构成为一个不可变的类。该类的单个实例将被创建一次,并且只有对该类的引用将被复制到赋值和参数传递中。

如果对象是可变的,那么这两种情况下的行为会有所不同:当一个人改变对象时,在第一种情况下,结构的副本将被修改,而在第二种情况下,原始对象将被改变。但是,在这两种情况下,对象都是不可变的,因此这个对象的用户实际上是类还是结构没有区别。

由于复制引用比复制struct便宜,为什么要使用不可变结构?

此外,从mutable structs are evil开始,看起来根本没有理由使用结构。

我哪里错了?

4 个答案:

答案 0 :(得分:30)

  

由于复制引用比复制struct便宜,为什么要使用不可变结构?

这并非总是如此。在64位操作系统上复制引用将是8个字节,这可能比许多结构更大。

另请注意,创建课程可能更昂贵。创建结构通常完全在堆栈上完成(尽管there are many exceptions),这非常快。创建类需要创建对象句柄(用于垃圾收集器),在堆栈上创建引用,以及跟踪对象的生命周期。这可能会增加GC压力,这也会产生实际成本。

话虽这么说,创建大型不可变结构可能不是一个好主意,这是Guidelines for choosing between Classes and Structures建议总是使用类的原因的一部分,如果你的结构将超过16字节,如果它将被装箱,以及其他使差异变小的问题。

话虽这么说,我经常将我的决定更多地放在有关类型的预期用途和含义上。值类型应该用于引用单个值(同样,参考指南),并且通常具有语义含义和预期用法不同于类。在类或结构体之间进行选择时,这通常与性能特征一样重要。

答案 1 :(得分:23)

里德的答案非常好,但只是增加几点:

  如果我错了,请纠正我

你基本上走在正确的轨道上。您已经犯了将变量混淆的常见错误。变量是存储位置;值存储在变量中。而你正在调整常见的神话,即“价值类型在堆栈上”;相反,变量短期长期存储上,因为变量是存储位置。变量是短期存储还是长期存储取决于其已知生存期,而不是类型

但所有这一切与你的问题并不特别相关,问题归结为要求驳斥这种三段论:

  • 可变结构是邪恶的。
  • 引用复制比结构复制便宜,因此不可变结构总是更糟。
  • 因此,结构从未使用过。

我们可以用几种方式驳斥三段论。

首先,是的,可变结构是邪恶的。但是,它们有时非常有用,因为在某些有限的情况下,您可以获得性能优势。除非已经用尽其他合理的途径并且存在真正的性能问题,否则我不推荐这种方法。

其次,引用复制不一定比struct复制便宜。引用通常实现为4或8字节的托管指针(尽管这是一个实现细节;它们可以实现为不透明的句柄)。复制引用大小的结构既不比复制引用大小的引用更便宜也不昂贵。

第三,即使引用复制比结构复制便宜,引用也必须取消引用才能进入其字段。解除引用不是零成本!不仅需要机器周期来取消引用引用,这样做可能会破坏处理器缓存,这可能会使未来的引用更加昂贵!

第四,即使参考复制比结构复制便宜,谁在乎呢?如果这不是产生不可接受的性能成本的瓶颈,那么哪一个更快就完全无关紧要。

第五,参考文献在内存空间中远远超过结构体。

第六,引用增加了费用,因为引用网络必须由垃圾收集器定期跟踪;垃圾收集器可以完全忽略“blittable”结构。垃圾收集费用很高。

第七,与引用类型不同,不可变值类型不能为null。你知道每个价值都是一个很好的价值。正如Reed所指出的,为了获得引用类型的良好价值,您必须运行分配器和构造函数。那不便宜。

第八,值类型代表值,程序通常是关于值的操纵。在一种语言中“烘焙”“价值”和“参考”的隐喻是有道理的,无论哪种“更便宜”。

答案 2 :(得分:2)

来自MSDN;

  

类是引用类型,结构是值类型。参考   类型在堆上分配,内存管理由处理   垃圾收集器。值类型在堆栈上分配   内联并在超出范围时被释放。一般来说,   值类型分配和取消分配更便宜。但是,如果他们   用于需要大量拳击的场景   取消装箱后,与参考类型相比,它们表现不佳。

除非类型具有以下所有特征,否则不要定义结构:

  • 它逻辑上表示单个值,类似于基本类型(整数,双精度等)。

  • 它的实例大小小于16个字节。

  • 这是不可改变的。

  • 不必频繁装箱。

因此,如果你的struct超过16个字节,你应该总是使用一个类而不是struct。另请阅读http://www.dotnetperls.com/struct

答案 3 :(得分:1)

结构有两种用例。不透明结构对于可以使用不可变类实现的东西很有用,但是它们足够小,即使在最好的情况下,使用类也不会有太大的好处 - 特别是如果频率很高它们被创建和丢弃的频率是它们被简单复制的频率的很大一部分。例如,Decimal是一个16字节的结构,因此保存一百万Decimal个值需要16兆字节。如果它是一个类,则对Decimal实例的每个引用将占用4或8个字节,但每个不同的实例可能需要另外20-32个字节。如果有一个大型数组,其元素是从少量不同的Decimal实例中复制的,那么该类可能会胜出,但在大多数情况下,一个人更有可能拥有一百万个引用数百万的数组。 Decimal的实例,这意味着结构将胜出。

以这种方式使用结构通常只有在MSDN引用的指南适用时才会有用(尽管不变性指南主要是因为结构方法无法通过哪种方式表明它们可以修改底层结构)。如果最后三条准则中的任何一条都不适用,那么使用不可变类而不是结构可能会更好。但是,如果第一条准则不适用,则意味着不应该使用不透明的结构,但不应该使用类来代替。

在某些情况下,数据类型的目的只是将一组变量与管道磁带一起固定,以便它们的值可以作为一个单元传递,但它们仍然在语义上保持为不同的变量。例如,许多方法可能需要传递表示3d坐标的三个浮点数的组。如果想绘制一个三角形,传递三个Point3d参数比九个浮点数更方便。在许多情况下,这种类型的目的不是为了赋予任何特定领域的行为,而是简单地提供一种方便地传递物品的方法。在这种情况下,如果正确使用它们,结构可以提供比类更大的性能优势。一个结构应该代表三个与管道胶带固定在一起的double类型的变量应该只有三个double类型的公共字段。这样的结构将允许有效地执行两个常见操作:

  1. 给定一个实例,拍摄其状态的快照,以便可以修改实例而不会干扰快照
  2. 鉴于一个不再需要的实例,不知何故想出一个略有不同的实例

不可变类类型允许第一个以固定成本执行,而不管类所持有的数据量,但它们在第二个时效率低。变量应该表示的数据量越大,执行第一个操作时不可变类类型与结构的优势越大,执行第二个操作时暴露字段结构的优势就越大。

可变类类在第二个操作占主导地位的情况下可以很有效,并且第一个很少需要,但是对象可能很难在可变类对象中公开当前值而不将对象本身暴露给外部修改。

请注意,根据使用模式,大型外露场结构可能比不透明结构或类类型更有效。大于17字节的结构通常比较小的结构效率低,但它们仍然比类更有效。此外,将结构作为ref参数传递的成本不依赖于其大小。如果通过属性而不是字段访问它们,通过值不必要地传递它们等等,那么大型结构是低效的。但是如果要小心避免多余的"复制"操作,有一些使用模式,其中类和结构没有收支平衡点 - 结构只会表现得更好。

有些人可能会对一种有暴露场的类型的想法感到恐惧,但我建议像我描述的这样的结构不应该被认为是一个实体本身,而是一个扩展读取或写入的东西例如:

public struct SlopeAndIntercept
{
   public double Slope,Intercept;
}
public SlopeAndIntercept FindLeastSquaresFit() ...

要执行一堆点的最小二乘拟合的代码将需要做大量的工作来找到结果线的斜率或Y截距;发现两者不会花费更多。调用FindLeastSquaresFit方法的代码可能希望在一个变量中具有斜率而在另一个变量中具有截距。如果这样的代码确实如此:

var resultLine = FindLeastSquaresFit();

结果将有效地创建两个变量resultLine.SloperesultLine.Intercept,该方法可以根据需要进行操作。 resultLine的字段实际上不属于SlopeIntercept,也属于FindLeastSquaresFit;它们属于声明resultLine的代码。情况与使用该方法的情况略有不同:

double Slope, Intercept;
FindLeastSquaresFit(out Slope, out Intercept);

在该上下文中,很明显紧接在函数调用之后,这两个变量具有该方法赋予的含义,但是它们在任何其他时间的含义将取决于该方法对它们的其他作用。同样对于上述结构的领域。

在某些情况下,使用不可变类而不是透明结构返回数据可能更好。除此之外,使用类将使返回Foo的函数的未来版本更容易返回包含附加信息的内容。另一方面,在很多情况下,代码会期望处理一组特定的离散事物,而改变这些事物会从根本上改变客户对它的处理方式。例如,如果有一堆代码处理(x,y)点,则添加" z"坐标将要求重写代码,并且" point"类型可以做到这一点。