用泛型装箱和拆箱


67

创建整数集合的.NET 1.0方法(例如)是:

ArrayList list = new ArrayList();
list.Add(i);          /* boxing   */
int j = (int)list[0]; /* unboxing */

使用此方法的代价是由于装箱和拆箱而导致缺乏类型安全性和性能。

.NET 2.0方法是使用泛型:

List<int> list = new List<int>();
list.Add(i);
int j = list[0];

装箱的价格(据我了解)是需要在堆上创建一个对象,将分配给堆栈的整数复制到新对象,反之亦然。

泛型的使用如何克服这个问题?堆栈分配的整数是否保留在堆栈上并从堆中指向(我想不是这样,因为当超出范围时会发生什么)?似乎仍然需要将其复制到堆栈中的其他位置。

到底是怎么回事?

Answers:


70

对于集合,泛型可以通过T[]内部利用实际数组来避免装箱/拆箱。List<T>例如,使用T[]数组存储其内容。

阵列,当然,是引用类型,因此(在CLR中,内容十分重要的当前版本)存储在堆上。但是由于它是aT[]而不是an object[],因此可以“直接”存储数组的元素:也就是说,它们仍在堆上,但是它们在数组的堆上而不是装箱并让数组包含对那些盒子。

因此List<int>,例如对于,数组中的内容将“看起来”如下:

[1 2 3]

ArrayList其与使用进行比较的,object[]然后将“看起来”如下所示:

[* a * b * c]

... where*a等是对对象的引用(装箱的整数):

* a-> 1
* b-> 2
* c-> 3

请原谅这些插图;希望你明白我的意思。


1
只是为了添加为什么List <int>被存储为值数组(例如:[1 2])。当JIT编译器看到List <int>时,它将代码重写为int [],当看到List <CustomClass>时,它将重写为CustomClass []。前者(值类型)是值的数组,而后者是指针的数组。ArrayList是普通类,它将所有内容存储为引用数组。这就是存储值类型变得困难的地方。
拉加夫2014年

72

您的困惑是由于误解了堆栈,堆和变量之间的关系而导致的。这是思考它的正确方法。

  • 变量是具有类型的存储位置。
  • 变量的生命周期可以短也可以长。“短”表示“直到当前函数返回或抛出”,而“长”表示“可能比这更长”。
  • 如果变量的类型是引用类型,则变量的内容是对长期存储位置的引用。如果变量的类型是值类型,则变量的内容是值。

作为实现细节,可以在栈上分配保证寿命短的存储位置。在堆上分配了可能长期存在的存储位置。注意,这并没有说明“值类型始终分配在堆栈上”。值类型并不总是分配在堆栈上:

int[] x = new int[10];
x[1] = 123;

x[1]是一个存储位置。它是长寿的。它的寿命可能比此方法更长。因此,它必须在堆上。它包含一个int的事实是无关紧要的。

您正确地说了为什么装箱的int价格昂贵:

装箱的代价是需要在堆上创建一个对象,将分配给堆栈的整数复制到新对象,反之亦然。

您出错的地方是说“堆栈分配的整数”。整数在哪里分配都没有关系。重要的是它的存储包含整数,而不是包含对堆位置的引用。价格是创建对象并进行复制的需要;这是唯一相关的成本。

那么,为什么通用变量不昂贵?如果您具有类型T的变量,并且T被构造为int,则您具有类型int,句点的变量。类型为int的变量是一个存储位置,它包含一个int。该存储位置是在堆栈上还是在堆上是完全不相关的。相关的是,存储位置包含一个int,而不是包含对堆上某些内容的引用。由于存储位置包含int,因此您不必承担装箱和拆箱的费用:在堆上分配新的存储并将int复制到新的存储。

现在清楚了吗?


3
感谢您强调值类型变量保存值,而引用类型变量保存对其他地方保存的信息的引用。值类型和引用类型之间的区别不在于它们是存储在堆栈中还是堆中,而是它们是否存储在变量中。引用类型的自动变量通常将存储在堆栈中,但是由它表示的信息将存储在堆中。
supercat

3

泛型允许输入列表的内部数组int[]而不是有效类型object[],这需要装箱。

没有泛型会发生以下情况:

  1. 你打电话Add(1)
  2. 将整数1装箱到一个对象中,该对象需要在堆上构造一个新对象。
  3. 该对象传递给ArrayList.Add()
  4. 装箱的对象已塞入object[]

这里有三个间接级别:ArrayList-> object[]-> object-> int

使用泛型:

  1. 你打电话Add(1)
  2. 将int 1传递给List<int>.Add()
  3. 将int塞入int[]

因此,只有两个级别的间接:List<int>-> int[]-> int

其他一些区别:

  • 非通用方法将需要8或12个字节的总和(一个指针,一个int)来存储值,一种分配为4/8,另一种分配为4。而且这可能更多是由于对齐和填充。通用方法在数组中仅需要4个字节的空间。
  • 非泛型方法需要分配一个装箱的int。通用方法没有。这样可以更快并减少GC流失。
  • 非泛型方法需要强制转换以提取值。这不是类型安全的,并且速度稍慢。

您的“间接级别”仅在逻辑上是正确的。对象实际上存储了int,没有间接。如果不谈论指针和坚韧的cookie,就无法真正解决此问题。
汉斯·帕桑

@汉斯 确实可以,但是运行时必须从对象中提取值,并且这将作为取消引用,因为指向对象的指针需要取消引用。(与在这种情况下A从对类的引用中提取字段相同Fooclass Foo { public int A; }。)
cdhowie 2010年

3

ArrayList仅处理类型,object因此要使用此类,需要强制转换为from和from object。对于值类型,此转换涉及装箱和拆箱。

当您使用通用列表时,编译器将为该值类型输出专用代码,以便将实际值存储在列表中,而不是对包含这些值的对象的引用。因此,不需要装箱。

装箱的价格(据我了解)是需要在堆上创建一个对象,将分配给堆栈的整数复制到新对象,反之亦然。

我认为您假设值类型总是在堆栈上实例化。事实并非如此-它们可以在堆,堆栈或寄存器中创建。有关此的更多信息,请参见Eric Lippert的文章:关于值类型的真相


因此,该整数仍从堆栈中复制到堆中,但是没有创建新对象吗?
Itay Karo 2010年

AFAIK值类型将作为其他对象的一部分分配到堆上。但是它们总是总是在堆栈上分配的局部变量或方法参数,对吗?
Itay Karo 2010年

您正在考虑对象的存储位置。泛型的性能更好,不是因为它们在哪里存储对象,而是因为WHAT存储在集合中。请参阅下面的答案。
Unmesh Kondolikar,2010年

1
如果它们在堆的堆栈中,为什么会很重要?这两种方式都不应该与您有关。
Dismissile

1

在.NET 1中,Add调用该方法时:

  1. 在堆上分配空间;新参考
  2. i变量的内容被复制到引用中
  3. 参考文献的副本放在列表的末尾

在.NET 2中:

  1. 变量的副本i传递给Add方法
  2. 该副本的副本放在列表的末尾

是的,i变量仍然被复制(毕竟,它是一个值类型,并且值类型总是被复制-即使它们只是方法参数)。但是堆上没有多余的副本。


1

您为什么要考虑WHERE存储值\对象?在C#中,值类型可以存储在堆栈中,也可以存储在堆中,具体取决于CLR选择什么。

泛型的不同之WHAT处存储在集合中。在ArrayList集合中包含对装箱对象的引用,其中List<int>包含int值本身。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.