关于c#:使用泛型装箱和拆箱

Boxing and unboxing with generics

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

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

使用此项的惩罚是由于装箱和拆箱而缺乏类型安全性和性能。

.NET 2.0的方法是使用泛型:

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

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

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

到底发生了什么?


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

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

例如,对于一个List,您在数组中所拥有的将"看起来"如下:

1
[ 1 2 3 ]

将其与使用object[]ArrayList相比,object[]因此"看起来"类似于:

1
[ *a *b *c ]

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

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

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


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

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

作为一个实现细节,可以在堆栈上分配一个保证寿命短的存储位置。堆上分配了可能长期存在的存储位置。请注意,这与"值类型总是在堆栈上分配"无关。值类型并不总是在堆栈上分配:

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

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

你说得对,为什么盒装int很贵:

The price of boxing is the need to create an object on the heap, copy the stack allocated integer to the new object and vice-versa for unboxing.

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

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

现在明白了吗?


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

以下是没有仿制药的情况:

  • 你叫Add(1)
  • 整数1被装箱到一个对象中,该对象要求在堆上构造一个新对象。
  • 此对象传递给ArrayList.Add()
  • 盒装物品被塞进一个object[]中。
  • 这里有三个间接层次:ArrayList->object[]->object->int

    泛型:

  • 你叫Add(1)
  • int 1传递给List.Add()
  • int被塞进一个int[]中。
  • 因此只有两个间接的层次:List->int[]->int

    其他一些差异:

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


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

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

    The price of boxing (to my understanding) is the need to create an object on the heap, copy the stack allocated integer to the new object and vice-versa for unboxing.

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


    为什么你会考虑用WHERE来存储值对象?在C中,值类型可以存储在堆栈和堆上,这取决于CLR选择的内容。

    其中泛型起作用的是WHAT存储在集合中。对于ArrayList的情况,集合包含对装箱对象的引用,因为List本身包含int值。


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

  • 堆上分配了空间;进行了新的引用
  • i变量的内容复制到引用中
  • 参考文献的副本放在清单的末尾。
  • 在.NET 2中:

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