值类型相对于引用类型的优势是什么?

6
考虑到每次将值类型作为参数传递时都会创建新实例,我开始思考在使用"ref"或"out"关键字的情况下会出现什么样的性能提升。经过一段时间的思考后,我意识到尽管我看到了使用值类型的不足之处,但我并不知道任何优点。
那么我的问题非常直接 - "拥有值类型的目的是什么?通过复制结构而不是仅创建对它的新引用,我们得到了什么好处?"。
在我看来,只拥有像Java中那样的引用类型会更容易。
编辑:为了澄清这一点,我不是指小于8个字节(引用的最大大小)的值类型,而是指大于或等于8个字节的值类型。
例如-包含四个"int"值的"Rectangle"结构。

5
如果一个数组是值类型,那么一百万个字节占用多少字节?如果是引用类型呢?
  • 如果数组是值类型,它将占用 1000000 字节(即 1 MB)的空间。
  • 如果数组是引用类型,则每个元素占用 4 个字节的空间用于存储指向实际数据的指针,因此总空间将增加 4 MB。
- Eric Lippert
是的,这有时会成为一个问题。即使DateTime也是一个对象。 - usr
@usr:“甚至DateTime也是一个对象” - 不确定你想表达什么。 - BrokenGlass
2
@dtryon:在Java中,所有用户定义的类型都是引用类型,但原始类型(如int)不是。 - Ben Voigt
@dtryon 除了原始值(intbooldouble等),在Java中一切都是引用类型。 - Acidic
显示剩余3条评论
6个回答

14
  • 一个一字节值类型的实例占用一个字节的空间。引用类型则占用引用本身的空间以及同步块和虚拟函数表等额外空间...

  • 复制引用时,只需复制四(或八)个字节的引用。要复制四字节整数,只需复制四个字节的整数。复制小型值类型的成本不比复制引用高。

  • 不包含引用的值类型根本不需要被垃圾回收器检查。每个引用都必须由垃圾回收器跟踪。


1
参考局部性也非常重要。 - Ben Voigt
我一开始应该明确指出,但我是在指比引用大小还要大的值类型。 关于你的第三点:一个方法本地的引用会在方法结束时自动丢弃,就像结构体一样,对吗? - Acidic
@Acidic:当然,引用被丢弃了。但是所指的东西可能没有被丢弃;其他东西可能在引用它。 - Eric Lippert
酸性的,不是。这种优化被称为逃逸分析。据我所知,一些JVM上已经实现了这个功能,但微软的JIT没有。我不清楚Mono的情况。 - usr
@EricLippert 确实如此,但为什么它比“其他东西可能持有值的副本”更糟糕呢?(我知道它是不同的,但这通常是我们对于复杂结构想要的)我只是觉得很难为除了最基本的原始值之外的任何内容辩护值类型。似乎更麻烦而已。 - Acidic
4
@Acidic:您完全正确。值类型仅在少数情况下是合理的,而这些情况大多已由内置的值类型覆盖。但是当您确实需要它们时,它们非常方便。 - Eric Lippert

4

值类型通常比引用类型更具性能优势:

  • 引用类型需要额外的内存进行引用和解引用时的性能开销。

  • 值类型不需要额外的垃圾回收,它会与其所在的实例一起进行垃圾回收。方法中的局部变量在方法结束时被清除。

  • 值类型数组与缓存结合使用非常高效。(例如,将整数数组与 Integer 类型的实例数组相比较)


3
"创建引用并不是问题,这只是32/64位的副本。实际上创建对象很便宜,但回收对象却很昂贵。当值类型较小且经常被丢弃时,它们对性能有利。它们可以在大型数组中非常高效地使用。结构体没有对象头。还有很多其他性能差异。编辑:Eric Lippert在评论中提出了一个很好的例子:“如果一百万个字节是值类型,则需要多少字节?如果是引用类型,则需要多少字节?”我的回答是:如果结构体打包设置为1,这样的数组将占用100万个字节和16个字节(在32位系统上)。使用引用类型将占用: "
array, object header: 12
array, length: 4
array, data: 4*(1 million) = 4m
1 million objects, headers = 12 * (1 million)
1 million objects, data padded to 4 bytes: 4 * (1 million)

这就是为什么在大型数组中使用值类型 可能 是一个好主意。


2

如果您的数据很小(<16字节),您有很多实例和/或经常操作它们,特别是传递给函数,那么收益将可见。这是因为与创建小值类型实例相比,创建对象相对昂贵。正如其他人指出的那样,对象需要被收集,这甚至更加昂贵。此外,非常小的值类型所占用的内存比它们的引用类型等效物要少。

.NET中非原始值类型的示例是Point结构(System.Drawing)。


1

每个变量都有其生命周期,但并非每个变量都需要灵活性以使您的变量执行高效,但不在堆中进行管理。

值类型(结构体)包含其数据分配在堆栈中或在结构体中内联分配。引用类型(类)存储对值的内存地址的引用,并分配在堆上。

拥有值类型的目的是什么? 值类型非常有效地处理简单数据,(应该用于表示不可变类型来表示值)

值类型对象不能分配在垃圾回收的堆上,表示对象的变量不包含指向对象的指针;变量包含对象本身。

与仅创建对其的新引用相比,复制结构体可以获得什么好处?

如果复制结构体,则C#将创建对象的新副本,并将对象的副本分配给单独的结构体实例。但是,如果复制类,则C#将创建对对象的引用的新副本,并将引用的副本分配给单独的类实例。结构体不能有析构函数,但类可以有析构函数。


1
一个主要的优点是值类型,比如Rectangle,如果有n个类型为Rectangle的存储位置,那么可以确定有n个不同的Rectangle实例。如果有一个长度至少为2的类型为Rectangle的数组MyArray,像MyArray[0] = MyArray[1]这样的语句将会把MyArray[1]的字段复制到MyArray[0]的字段中,但它们仍然指向不同的Rectangle实例。如果执行一条语句MyArray[0].X += 4,那么将修改一个实例的X字段,而不会修改任何其他数组槽或Rectangle实例的X值。需要注意的是,创建数组时会立即填充可写的Rectangle实例。
假设Rectangle是可变的类类型。创建一个包含可变Rectangle实例的数组需要首先给数组一个维度,然后为数组中的每个元素分配一个新的Rectangle实例。如果想要将一个矩形实例的值复制到另一个矩形实例,就必须执行类似于MyArray[0].CopyValuesFrom(MyArray[1]) 的操作(如果MyArray[0] 没有被赋予对一个新实例的引用,这当然会失败)。如果不小心执行了MyArray[0] = MyArray[1],那么写入MyArray[0].X也会影响MyArray[1].X。这很恶心。

需要注意的是,在C#和vb.net中有一些地方,编译器会隐式复制值类型,然后像原始值一样对副本进行操作。这是一种非常不幸的语言设计,促使一些人提出了价值类型应该是不可变的建议(因为大多数涉及隐式复制的情况只会导致可变值类型的问题)。在编译器非常糟糕的警告语义上可疑的复制将产生错误行为的情况下,这种想法可能是合理的。然而,今天应该被认为已经过时,因为任何体面的现代编译器都会在大多数情况下标记错误,其中隐式复制将产生破碎的语义,包括所有仅通过构造函数、属性设置器或公共可变字段的外部赋值来改变结构的情况。像MyArray[0].X += 5这样的语句比MyArray[0] = new Rectangle(MyArray[0].X + 5, MyArray[0].Y, MyArray[0].Width, MyArray[0].Height)更易读。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接