首页 文章

拳击和拆箱与泛型

提问于
浏览
58

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

拳击的价格(据我所知)是需要在堆上创建一个对象,将堆栈分配的整数复制到新对象,反之亦然,以便取消装箱 .

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

真的发生了什么?

6 回答

  • 3

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

    • 在堆上分配空间;一个新的参考

    • i 变量的内容被复制到引用中

    • 引用的副本放在列表的末尾

    在.NET 2中:

    • 变量 i 的副本传递给 Add 方法

    • 该副本的副本放在列表的末尾

    是的,仍然会复制 i 变量(毕竟,'s a value type, and value types are always copied - even if they'只是方法参数) . 但是堆上没有冗余的副本 .

  • 62

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

    泛型有所不同的地方是 WHAT 存储在集合中 . 在 ArrayList 的情况下,集合包含对盒装对象的引用,其中 List<int> 包含int值本身 .

  • 3

    ArrayList只处理 object 类型,因此要使用此类需要转换为 object . 在值类型的情况下,此转换涉及装箱和拆箱 .

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

    拳击的价格(据我所知)是需要在堆上创建一个对象,将堆栈分配的整数复制到新对象,反之亦然,以便取消装箱 .

    我认为你假设值类型总是在堆栈上实例化 . 情况并非如此 - 它们可以在堆上,堆栈上或寄存器中创建 . 有关这方面的更多信息,请参阅Eric Lippert的文章:The Truth About Value Types .

  • 63

    在集合方面,泛型可以通过内部利用实际的 T[] 数组来避免装箱/拆箱 . List<T> 例如使用 T[] 数组来存储其内容 .

    当然,数组是一个引用类型,因此(在当前版本的CLR中,yada yada)存储在堆上 . 但由于它是 T[] 而不是 object[] ,数组's elements can be stored 2859221 : that is, they'仍然在堆上,但是它们在数组的堆上而不是被装箱并且数组包含对这些框的引用 .

    所以对于一个 List<int> ,例如,你在数组中拥有的东西会像这样:"look":

    [ 1 2 3 ]
    

    将其与 ArrayList 进行比较,后者使用 object[] ,因此会像这样"look":

    [ *a *b *c ]
    

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

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

    请原谅那些粗略的插图;希望你知道我的意思 .

  • 1

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

    • 变量是具有类型的存储位置 .

    • 变量的生命周期可以是短的也可以是长的 . "short"表示"until the current function returns or throws","long"表示"possibly longer than that" .

    • 如果变量的类型是引用类型,则变量的内容是对长期存储位置的引用 . 如果变量的类型是值类型,则变量的内容是值 .

    作为实现细节,可以在堆栈上分配保证短期存储的存储位置 . 在堆上分配可能长寿的存储位置 . 请注意,这没有说明"value types are always allocated on the stack."值并不总是在堆栈上分配值类型:

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

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

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

    装箱的价格是需要在堆上创建一个对象,将堆栈分配的整数复制到新对象,反之亦然,以便取消装箱 .

    你出错的地方就是说"the stack allocated integer" . 分配整数的位置无关紧要 . 重要的是它的存储包含整数,而不是包含对堆位置的引用 . 价格是需要创建对象并进行复制;这是唯一相关的成本 .

    那么为什么通用变量代价不高呢?如果你有一个T类型的变量,并且T被构造为int,那么你有一个int,period类型的变量 . int类型的变量是存储位置,它包含int . Whether that storage location is on the stack or the heap is completely irrelevant . 相关的是存储位置包含一个int,而不是包含对堆上某些东西的引用 . 由于存储位置包含int,因此您不必承担装箱和拆箱的成本:在堆上分配新存储并将int复制到新存储 .

    那现在清楚了吗?

  • 1

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

    这是没有泛型的情况:

    • 你打电话 Add(1) .

    • 整数 1 被装入一个对象,这需要在堆上构造一个新对象 .

    • 此对象传递给 ArrayList.Add() .

    • 盒装对象被塞入 object[] .

    这里有三个间接级别: ArrayList - > object[] - > object - > int .

    使用泛型:

    • 你打电话 Add(1) .

    • int 1传递给 List<int>.Add() .

    • int填入 int[] .

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

    其他一些差异:

    • 非泛型方法需要8或12个字节(一个指针,一个int)的总和来存储值,4/8存储在一个分配中,4个存储在另一个分配中 . 这可能更多是由于对齐和填充 . 泛型方法在数组中只需要4个字节的空间 .

    • 非泛型方法需要分配一个盒装的int;通用方法没有 . 这样可以更快地减少GC流失 .

    • 非泛型方法需要强制转换来提取值 . 这不是类型安全的,而且速度有点慢 .

相关问题