与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的情况:
- 你可以调用
Add(1)
。 - 整数
1
被装箱成一个对象,这就需要在堆上构build一个新的对象。 - 这个对象被传递给
ArrayList.Add()
。 - 盒装的对象被塞进一个
object[]
。
这里有三个间接的层次: ArrayList
– > object[]
– > object
– > int
。
用generics:
- 你可以调用
Add(1)
。 - int 1传递给
List<int>.Add()
。 - 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
方法时:
- 空间分配在堆上; 提出新的参考
-
i
variables的内容被复制到参考中 - 参考文献的副本放在列表的末尾
在.NET 2中:
- 将variables
i
副本传递给Add
方法 - 该副本的副本放在列表的末尾
是的, i
variables仍然被复制(毕竟,它是一个值types,值types总是被复制 – 即使它们只是方法参数)。 但是堆上没有多余的副本。
为什么你想在WHERE
存储值\对象? 在C#中,值types可以存储在堆栈和堆中,具体取决于CLRselect的内容。
凡generics有所作为的是WHAT
被存储在集合中。 在ArrayList
的情况下,集合包含对装箱对象的引用,因为List<int>
本身包含int值。