关于c#:不可变类vs结构

Immutable class vs struct

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

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

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

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

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

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

而且,由于可变结构是邪恶的,所以看起来根本没有理由使用结构。

我哪里错了?


Since copying reference is cheaper than copying struct, why would one use an immutable struct?

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

还要注意,创建类的成本可能更高。创建结构通常完全在堆栈上完成(尽管有许多异常),这非常快。创建类需要创建对象句柄(对于垃圾收集器),在堆栈上创建引用,并跟踪对象的生存期。这可以增加GC压力,这也有实际的成本。

也就是说,创建一个大的不可变结构可能不是一个好主意,这也是为什么在类和结构之间选择的指导原则建议始终使用类的一部分,如果您的结构将超过16个字节,如果它将被装箱,以及其他使差异更小的问题。

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


里德的回答很好,但只是增加了几点:

please correct me if I'm wrong

你基本上走对了。你犯了一个常见的错误,把变量和值混淆在一起。变量是存储位置;值存储在变量中。你在调侃一个常见的误区,即"价值类型在堆栈上";相反,变量要么进行短期存储,要么进行长期存储,因为变量是存储位置。变量是短期存储还是长期存储取决于其已知的生存期,而不是其类型。

但所有这些都与你的问题无关,你的问题归根结底就是要反驳这个三段论:

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

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

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

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

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

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

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

第六,引用增加了开销,因为引用网络必须由垃圾收集器定期跟踪;"blittable"结构可能被垃圾收集器完全忽略。垃圾收集费用很大。

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

第八,值类型代表值,程序通常是关于值的操作。不管哪种语言"便宜",在一种语言中"价值"和"参考"的隐喻中"烘焙"都是有意义的。


来自MSDN;

Classes are reference types and structures are value types. Reference
types are allocated on the heap, and memory management is handled by
the garbage collector. Value types are allocated on the stack or
inline and are deallocated when they go out of scope. In general,
value types are cheaper to allocate and deallocate. However, if they
are used in scenarios that require a significant amount of boxing and
unboxing, they perform poorly as compared to reference types.

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

  • 它在逻辑上表示单个值,类似于基元类型(integer、double等)。

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

  • 它是不变的。

  • 它不必经常装箱。

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


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

以这种方式使用结构通常只有在msdn引用的指导原则适用的情况下才是好的(尽管不可变指导原则主要是由于还没有任何方法可以指示结构方法修改基础结构这一事实的结果)。如果最后三条准则中的任何一条都不适用,那么使用不可变类可能比使用结构更好。但是,如果第一条准则不适用,这意味着不应使用不透明结构,而应使用类。好的。

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

  • 给定一个实例,获取其状态的快照,以便在不干扰快照的情况下修改该实例。
  • 给定一个不再需要的实例,以某种方式提出一个稍有不同的实例

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

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

    请注意,根据使用模式的不同,大型暴露字段结构可能比不透明结构或类类型更有效。大于17字节的结构通常比较小的结构效率低,但它们仍然比类效率高得多。此外,作为ref参数传递结构的成本并不取决于其大小。如果通过属性而不是字段访问大型结构,或者不必要地通过值传递大型结构,那么大型结构的效率就很低。但是,如果小心避免重复的"复制"操作,那么在使用模式中,类与结构之间没有盈亏平衡点——结构只会执行得更好。好的。

    有些人可能会因为一种类型具有暴露的字段而恐惧地退缩,但我建议像我所描述的结构不应该被认为是一个独立的实体,而应该是读或写它的东西的扩展。例如:好的。

    1
    2
    3
    4
    5
    public struct SlopeAndIntercept
    {
       public double Slope,Intercept;
    }
    public SlopeAndIntercept FindLeastSquaresFit() ...

    要执行一组点的最小二乘拟合的代码必须做大量的工作才能找到结果线的斜率或y截距;两者都不需要花费太多的成本。调用FindLeastSquaresFit方法的代码可能希望在一个变量中有坡度,在另一个变量中有截距。如果这样的代码:好的。

    1
    var resultLine = FindLeastSquaresFit();

    结果将有效地创建两个变量resultLine.SloperesultLine.Intercept,该方法可以在其认为合适的情况下进行操作。resultLine的字段实际上不属于SlopeIntercept,也不属于FindLeastSquaresFit;它们属于声明resultLine的代码。这种情况与使用该方法的情况略有不同:好的。

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

    在这种情况下,很明显,在函数调用之后,这两个变量具有方法所赋予的含义,但它们在任何其他时间的含义将取决于方法对它们所做的其他操作。同样适用于上述结构的领域。好的。

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