拳击和拆箱与泛型

时间:2010-12-09 20:59:13

标签: c# .net generics boxing unboxing

.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];

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

仿制药的使用如何克服这个问题?堆栈分配的整数是否保留在堆栈上并从堆中指向(我想这不是这种情况,因为当它超出范围时会发生什么)?看起来仍然需要将其复制到堆栈外的其他地方。

真正发生了什么?

6 个答案:

答案 0 :(得分:64)

当涉及到集合时,泛型可以通过在内部利用实际的T[]数组来避免装箱/取消装箱。例如List<T>使用T[]数组来存储其内容。

数组当然是一种引用类型,因此(在当前版本的CLR中,yada yada)存储在堆上。但由于它是T[]而不是object[],因此数组的元素可以“直接”存储:也就是说,它们仍然在堆上,但它们位于堆中数组而不是盒装,并且数组包含对框的引用。

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

[ 1 2 3 ]

将此与使用ArrayList的{​​{1}}进行比较,因此会“看起来”像这样:

[ *a *b *c ]

...其中object[]等是对象的引用(盒装整数):

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

原谅那些粗略的插图;希望你知道我的意思。

答案 1 :(得分:63)

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

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

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

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

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

你正确地说出为什么盒装int很贵:

  

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

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

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

现在清楚了吗?

答案 2 :(得分:3)

ArrayList只处理类型object,因此要使用此类需要在object之间进行转换。在值类型的情况下,此转换涉及装箱和拆箱。

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

  

拳击的价格(据我所知)是需要在堆上创建一个对象,将堆栈分配的整数复制到新对象,反之亦然,以便取消装箱。

我认为你假设值类型总是在堆栈上实例化。情况并非如此 - 它们可以在堆上,堆栈上或寄存器中创建。有关这方面的更多信息,请参阅Eric Lippert的文章:The Truth About Value Types

答案 3 :(得分:3)

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

以下是没有泛型的情况:

  1. 您致电Add(1)
  2. 整数1被装入一个对象,这需要在堆上构建一个新对象。
  3. 此对象传递给ArrayList.Add()
  4. 装箱的物品被塞进object[]
  5. 此处有三个间接级别:ArrayList - &gt; object[] - &gt; object - &gt; int

    使用泛型:

    1. 您致电Add(1)
    2. 将int 1传递给List<int>.Add()
    3. 将int填入int[]
    4. 因此只有两个间接级别:List<int> - &gt; int[] - &gt; int

      其他一些差异:

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

答案 4 :(得分:1)

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

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

    1. 变量i的副本将传递给Add方法
    2. 该副本的副本放在列表的末尾
    3. 是的,i变量仍然被复制(毕竟,它是一个值类型,并且值类型总是被复制 - 即使它们只是方法参数)。但是堆上没有冗余的副本。

答案 5 :(得分:1)

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

在泛型产生影响的地方,WHAT存储在集合中。在ArrayList的情况下,集合包含对盒装对象的引用,其中List<int>包含int值本身。