与generics拳击和拆箱

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

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

使用这个惩罚是由于拳击和拆箱造成的types安全和性能的缺乏。

.NET 2.0的方法是使用generics:

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

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

generics的使用如何克服这一点? 堆栈分配的整数是否停留在堆栈上,并从堆中指向(我想这不是因为它会超出范围会发生什么情况)? 似乎仍然需要将其复制到其他地方。

究竟是怎么回事?

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

当然,这个数组是一个引用types,因此(在当前版本的CLR中,yada yada)存储在堆中。 但是由于它是一个T[]而不是一个object[] ,所以数组的元素可以被直接存储:即它们仍然在堆上,但是它们在数组中,而不是被装箱让数组包含引用框。

所以对于一个List<int> ,例如,你在数组中会看起来像这样:

 [1 2 3]

将它与一个使用object[]ArrayList进行比较,因此会“看起来”像这样:

 [* a * b * c]

…其中*a等是对象的引用(盒装整数):

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

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

你的困惑是误解堆栈,堆和variables之间关系的结果。 这是正确的思考方式。

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

作为一个实现细节,可以在堆栈上分配一个保证存活的存储位置。 一个可能长期存储的位置被分配到堆上。 注意这里没有提到“值types总是在堆栈上分配”。 值types并不总是分配在堆栈上:

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

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

你正确地说,为什么盒装int是昂贵的:

拳击的价格是需要在堆上创build一个对象,将堆栈分配的整数复制到新的对象,反之亦然拆箱。

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

那么为什么不是一个通用的variables成本高? 如果你有一个Ttypes的variables,并且T被构造为int,那么你有一个inttypes的variables,句点。 inttypes的variables是一个存储位置,它包含一个int。 该存储位置是堆栈还是堆是完全不相关的 。 什么是相关的是,存储位置包含一个int ,而不是包含在堆上的东西的引用 。 由于存储位置包含一个int,所以您不必承担装箱和拆箱的成本:在堆上分配新存储并将int复制到新存储。

现在清楚了吗?

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

以下是没有generics的情况:

  1. 你可以调用Add(1)
  2. 整数1被装箱成一个对象,这就需要在堆上构build一个新的对象。
  3. 这个对象被传递给ArrayList.Add()
  4. 盒装的对象被塞进一个object[]

这里有三个间接的层次: ArrayList – > object[] – > object – > int

用generics:

  1. 你可以调用Add(1)
  2. int 1传递给List<int>.Add()
  3. int被塞进int[]

所以只有两个间接级别: List<int> – > int[] – > int

其他一些差异:

  • 非generics方法需要8或12个字节(一个指针,一个int)来存储值,一个分配4/8,另一个分配4个。 这可能会更多,由于alignment和填充。 通用方法只需要数组中的4个字节的空间。
  • 非generics方法需要分配一个盒装的int; 通用的方法不。 这是更快,并减lessGC搅动。
  • 非generics方法需要强制转换以提取值。 这不是types安全的,它有点慢。

一个ArrayList只处理typesobject所以要使用这个类需要从object 。 在值types的情况下,这个投射涉及装箱和拆箱。

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

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

我认为你假设值types总是在堆栈上实例化。 情况并非如此 – 它们可以在堆上,堆栈中或寄存器中创build。 欲了解更多信息,请参阅Eric Lippert的文章: 关于价值types的真相 。

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

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

在.NET 2中:

  1. 将variablesi副本传递给Add方法
  2. 该副本的副本放在列表的末尾

是的, ivariables仍然被复制(毕竟,它是一个值types,值types总是被复制 – 即使它们只是方法参数)。 但是堆上没有多余的副本。

为什么你想在WHERE存储值\对象? 在C#中,值types可以存储在堆栈和堆中,具体取决于CLRselect的内容。

凡generics有所作为的是WHAT被存储在集合中。 在ArrayList的情况下,集合包含对装箱对象的引用,因为List<int>本身包含int值。