Бокс и распаковка с дженериками


.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 53

6 ответов:

когда дело доходит до коллекций, дженерики позволяют избежать бокса / распаковки, используя фактические T[] массивы внутренне. List<T> на примере T[] массив для хранения его содержимого.

The массив, конечно, является ссылочным типом и поэтому (в текущей версии среды CLR, yada yada) хранится в куче. Но так как это T[], а не object[], элементы массива могут храниться "напрямую": то есть они все еще находятся в куче, но они на куче в массиве вместо того, чтобы быть в коробке и иметь массив, содержащий ссылки на коробки.

так List<int>, например, то, что у вас будет в массиве, будет "выглядеть" следующим образом:

[ 1 2 3 ]

сравните это с ArrayList, который использует object[] и поэтому будет "выглядеть" примерно так:

[ *a *b *c ]

...где *a и т. д. ссылки на объекты (целые числа в коробках):

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

извините эти грубые иллюстрации; надеюсь, вы понимаете, что я имею в виду.

ваша путаница является результатом непонимания того, что отношения между стеком, кучей и переменными. Вот правильный способ думать об этом.

  • переменная-это место хранения, которое имеет тип.
  • время жизни переменной может быть коротким или длинным. Под " коротким "мы подразумеваем" до тех пор, пока текущая функция не вернет или не бросит", а под" длинным "мы подразумеваем"возможно, дольше".
  • если тип переменной ссылочного типа тогда содержимое переменной является ссылкой на место длительного хранения. Если тип переменной является типом значения, то содержимое переменной является значением.

как деталь реализации, место хранения, которое гарантированно будет недолговечным, может быть выделено в стеке. Место хранения, которое может быть долговечным, выделяется в куче. Обратите внимание, что это ничего не говорит о "типы значений всегда выделяются в стеке."Типы значений являются не всегда выделяется в стеке:

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

x[1] - это место хранения. Он долговечен; он может жить дольше, чем этот метод. Поэтому он должен быть в куче. Тот факт, что он содержит int, не имеет значения.

вы правильно говорите, почему коробочный int стоит дорого:

цена бокса-это необходимость создать объект в куче, скопировать выделенное целое число стека в новый объект и наоборот для распаковки.

где вы ошибаетесь, чтобы сказать "стек выделенное целое число". Не имеет значения, где было выделено целое число. Важно было то, что его хранение содержал целое число, вместо того, чтобы содержать ссылка на расположение кучи. Цена-это необходимость создать объект и сделать копию; это единственная стоимость, которая имеет значение.

Так почему же общая переменная не является дорогостоящей? Если у вас есть переменная типа T, и T строится чтобы быть int, то у вас есть переменная типа int, период. Переменная типа int-это место хранения, и она содержит int. является ли это место хранения в стеке или куче совершенно не имеет значения. Важно то, что место хранения содержит int, вместо того, чтобы содержать ссылка на что-то в куче. Поскольку место хранения содержит int, вам не нужно брать на себя расходы на бокс и распаковку: выделение нового хранилища в куче и копирование int в новое хранилище.

теперь понятно?

Generics позволяет ввести внутренний массив списка 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; универсальный метод не делает. Это быстрее и уменьшает отток ГК.
  • не универсальный метод требуется приведение для извлечения значений. Это не типобезопасно, и это немного медленнее.

ArrayList обрабатывает только тип object поэтому для использования этого класса требуется приведение к и от object. В случае типов значений это приведение включает в себя бокс и распаковку.

при использовании универсального списка компилятор специализированный код для этого типа значения, так что фактические значения хранятся в списке, а не ссылки на объекты, содержащие значения. Поэтому никакой бокс не требуется.

цена бокса (to мое понимание) - это необходимость создать объект в куче, скопировать выделенное целое число стека в новый объект и наоборот для распаковки.

Я думаю, вы предполагаете, что типы значений всегда создаются в стеке. Это не так - они могут быть созданы либо в куче, либо в стеке, либо в регистрах. Для получения дополнительной информации об этом см. статью Эрика Липперта:Правда О Типах Значений.

в .NET 1, когда Add метод называется:

  1. в куче выделяется место; создается новая ссылка
  2. содержание i переменная копируется в ссылку
  3. копия ссылки помещается в конце списка

в .NET 2:

  1. копия переменной i перешло к Add метод
  2. копия этой копии помещается в конце список

да i переменная копируется (в конце концов, это тип значения и типы значений всегда копируются, даже если они просто параметры метода). Но в куче нет избыточной копии.

Почему вы думаете в терминах WHERE значения\объекты хранятся? В C# типы значений могут храниться как в стеке, так и в куче в зависимости от того, что выбирает среда CLR.

где дженерики имеют значение WHAT хранится в коллекции. В случае ArrayList коллекция содержит ссылки на упакованные объекты, где как List<int> содержит сами значения int.