使用泛型装箱和拆箱

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

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

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

到底发生了什么?

25061 次浏览

ArrayList 只处理 object类型,因此要使用这个类,需要在 object之间进行强制转换。对于值类型,此强制转换涉及装箱和取消装箱。

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

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

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

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

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

在.NET 2中:

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

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

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

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

例如,对于 List<int>,数组中的内容“看起来”是这样的:

[ 1 2 3 ]

比较一下 ArrayList,它使用 object[],因此看起来像这样:

[ *a *b *c ]

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

*a -> 1
*b -> 2
*c -> 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[]中。

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

还有一些不同之处:

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

为什么要用 WHERE来考虑值对象的存储呢?在 C # 中,值类型可以存储在堆栈中,也可以存储在堆中,这取决于 CLR 的选择。

泛型起作用的地方是 WHAT存储在集合中。对于 ArrayList,集合包含对装箱对象的引用,其中 List<int>包含 int 值本身。

您的困惑是由于误解了堆栈、堆和变量之间的关系。这是正确的思考方式。

  • 变量是具有类型的存储位置。
  • 变量的生存期可以短也可以长。“ short”的意思是“直到当前函数返回或抛出”,“ long”的意思是“可能比这长”。
  • 如果变量的类型是引用类型,那么变量的内容就是对长期存储位置的引用。如果变量的类型是值类型,那么变量的内容就是值。

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

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

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

你正确地解释了为什么盒装整型变量是昂贵的:

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

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

那么为什么泛型变量不昂贵呢?如果您有一个类型为 T 的变量,并且 T 被构造为 int,那么您就有一个类型为 int 的变量,句点。类型为 int 的变量是一个存储位置,它包含一个 int。无论存储位置是在堆栈上还是在堆上,都是完全无关的.相关的是存储位置 包含一个 int,而不是包含 对堆中某物的引用。由于存储位置包含 int,因此不必承担装箱和取消装箱的成本: 在堆上分配新存储并将 int 复制到新存储。

现在明白了吗?