72

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

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

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

到底发生了什么?

4

6 回答 6

78

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

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

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

[ 1 2 3 ]

将此与 an 进行比较ArrayList,后者使用 anobject[]并因此“看起来”如下所示:

[ *a *b *c ]

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

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

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

于 2010-12-09T21:04:48.057 回答
78

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

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

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

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

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

您正确地说明了为什么盒装 int 很昂贵:

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

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

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

现在清楚了吗?

于 2010-12-09T23:15:09.170 回答
3

ArrayList 仅处理类型object,因此要使用此类需要强制转换为 和 from object。在值类型的情况下,这种转换涉及装箱和拆箱。

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

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

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

于 2010-12-09T21:02:20.137 回答
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 被塞进一个int[].

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

其他一些区别:

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

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

  1. 在堆上分配空间;产生了新的参考
  2. 变量的内容i被复制到引用中
  3. 参考的副本放在列表的末尾

在 .NET 2 中:

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

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

于 2010-12-09T21:03:38.313 回答
1

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

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

于 2010-12-09T21:12:13.587 回答