25

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

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

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

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

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

既然复制引用比复制结构便宜,为什么要使用不可变结构呢?

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

我哪里错了?

4

4 回答 4

31

既然复制引用比复制结构便宜,为什么要使用不可变结构呢?

这并不总是正确的。在 64 位操作系统上复制引用将是 8 个字节,这可能比许多结构大。

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

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

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

于 2013-01-03T21:17:56.603 回答
26

里德的回答非常好,但只是补充几点:

如果我错了,请纠正我

你在这里基本上是在正确的轨道上。您犯了将变量混淆的常见错误。变量是存储位置;值存储在变量中。而且您正在与“值类型在堆栈上”的普遍说法调情;相反,变量要么进行短期存储,要么进行长期存储,因为变量是存储位置。变量是进行短期存储还是长期存储取决于其已知的生命周期,而不是其类型

但是所有这些都与您的问题并不特别相关,归结为要求驳斥这种三段论:

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

我们可以通过多种方式驳斥三段论。

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

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

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

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

第五,引用在内存空间中比结构要昂贵得多。

第六,引用增加了费用,因为引用网络必须由垃圾收集器定期跟踪;垃圾收集器可能会完全忽略“blittable”结构。垃圾回收是一笔大开销。

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

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

于 2013-01-04T06:13:51.183 回答
2

来自MSDN

类是引用类型,结构是值类型。引用类型在堆上分配,内存管理由垃圾收集器处理。值类型在堆栈或内联上分配,并在超出范围时被释放。通常,值类型的分配和解除分配成本更低。但是,如果在需要大量装箱和拆箱的场景中使用它们,则与引用类型相比,它们的性能较差。

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

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

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

  • 它是不可变的。

  • 它不必经常装箱。

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

于 2013-01-03T21:20:50.017 回答
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.Slope,并且resultLine.Intercept该方法可以根据需要对其进行操作。的领域resultLine不真正属于SlopeIntercept,也不属于FindLeastSquaresFit;它们属于声明的代码resultLine。如果将该方法用作以下情况,情况几乎没有什么不同:

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

在这种情况下,很明显,在函数调用之后,这两个变量具有由方法分配的含义,但它们在任何其他时间的含义将取决于方法对它们的其他作用。对于上述结构的字段也是如此。

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

于 2013-01-04T01:01:18.520 回答