不可变类vs结构

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

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

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

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

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

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

此外,由于可变结构是邪恶的 ,看起来根本没有理由使用结构。

我哪里错了?

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

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

另请注意,创建类可能更昂贵。 创建结构通常完全在堆栈上完成(尽管有很多例外 ),这非常快。 创建类需要创建对象句柄(用于垃圾收集器),在堆栈上创建引用,以及跟踪对象的生命周期。 这可能会增加GC压力,这也是一个真正的成本。

话虽这么说,创建一个大的不可变结构可能不是一个好主意,这是为什么在类和结构之间进行选择的指南建议总是使用一个类,如果你的结构将超过16个字节,如果它将被装箱,和其他使差异变小的问题。

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

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

如果我错了,请纠正我

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

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

  • 可变结构是邪恶的。
  • 引用复制比结构复制便宜,因此不可变结构总是更糟。
  • 因此,结构永远不会有任何用途。

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

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

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

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

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

第五,参考文献在内存空间中远比结构更昂贵。

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

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

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

来自MSDN ;

类是引用类型,结构是值类型。 引用类型在堆上分配,内存管理由垃圾收集器处理。 值类型在堆栈或内联中分配,并在超出范围时释放。 通常,值类型分配和取消分配更便宜。 但是,如果它们在需要大量装箱和拆箱的场景中使用,则与参考类型相比,它们表现不佳。

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

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

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

  • 这是不可改变的。

  • 它不必经常装箱。

因此,如果结构将超过16个字节,则应始终使用类而不是struct。 另请阅读http://www.dotnetperls.com/struct

结构有两种用例。 不透明结构对于可以使用不可变类实现的东西很有用,但是它们足够小,即使在最好的情况下也不会有太多 – 如果有的话 – 使用类的好处,特别是如果它们使用它们的频率被创建和丢弃的频率是它们被简单复制的频率的很大一部分。 例如, 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的字段resultLine不属于SlopeIntercept ,也不属于FindLeastSquaresFit ; 它们属于声明resultLine的代码。 情况与使用该方法时的情况略有不同:

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

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

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