Зачем некоторым языкам нужен бокс и распаковка?
Вопрос не в том, что такое бокс и распаковка, а в том, зачем это нужно таким языкам, как Java и C#?
Я очень хорошо знаком с C++, STL и Boost.
В C++ я мог бы написать что-то вроде этого очень легко,
std::vector<double> dummy;
У меня есть некоторый опыт работы с Java, но я был очень удивлен, потому что мне пришлось написать что-то вроде этого,
ArrayList<Double> dummy = new ArrayList<Double>();
Мой вопрос, почему это должен быть объект, что технически так сложно включить примитивные типы, когда речь идет о дженериках?
6 ответов
что так сложно технически включить примитивные типы, когда речь идет о дженериках?
В случае с Java это связано с тем, как работают дженерики. В Java дженерики - это трюк во время компиляции, который не позволяет вам Image
возражать в ArrayList<String>
, Тем не менее, универсальные шаблоны Java реализованы с помощью стирания типов: универсальная информация о типах теряется во время выполнения. Это было сделано из соображений совместимости, потому что дженерики были добавлены довольно поздно в жизни Java. Это означает, что во время выполнения ArrayList<String>
эффективно ArrayList<Object>
(или лучше: просто ArrayList
что ожидает и возвращает Object
во всех его методах), который автоматически приводит к String
когда вы получаете значение.
Но с тех пор int
не происходит от Object
, вы не можете поместить его в ArrayList, который ожидает (во время выполнения) Object
и вы не можете разыграть Object
в int
или. Это означает, что примитив int
должен быть обернут в тип, который наследуется от Object
, лайк Integer
,
C# например, работает по-другому. Обобщения в C# также применяются во время выполнения, и с List<int>
, Бокс в C# происходит только при попытке сохранить тип значения, такой как int
в переменной ссылочного типа, как object
, поскольку int
в C# наследуется от Object
в C# пишу object obj = 2
вполне допустимо, однако int будет упакован, что делается автоматически компилятором (нет Integer
Тип ссылки выставляется пользователю или что-либо).
Упаковка и распаковка являются необходимостью, обусловленной тем, как языки (такие как C# и Java) реализуют свои стратегии распределения памяти.
Некоторые типы размещаются в стеке, а другие - в куче. Чтобы рассматривать тип, выделенный из стека, как тип, выделенный из кучи, бокс должен перемещать тип, выделенный из стека, в кучу. Распаковка происходит в обратном порядке.
В C# типы, выделяемые стеком, называются типами значений (например, System.Int32
а также System.DateTime
) и типы, выделенные в куче, называются ссылочными типами (например, System.Stream
а также System.String
).
В некоторых случаях выгодно иметь возможность обрабатывать тип значения как ссылочный тип (отражение является одним из примеров), но в большинстве случаев лучше избегать упаковки и распаковки.
Каждый нестроковый нестроковый объект, хранящийся в куче, содержит 8- или 16-байтовый заголовок (размеры для 32/64-битных систем), за которым следует содержимое открытых и закрытых полей этого объекта. Массивы и строки имеют вышеуказанный заголовок, плюс еще несколько байтов, определяющих длину массива и размер каждого элемента (и, возможно, количество измерений, длину каждого дополнительного измерения и т. Д.), За которыми следуют все поля первого элемент, затем все поля второго и т. д. При наличии ссылки на объект система может легко проверить заголовок и определить, к какому типу он относится.
Места хранения ссылочного типа содержат четырех- или восьмибайтовое значение, которое однозначно идентифицирует объект, хранящийся в куче. В существующих реализациях это значение является указателем, но его проще (и семантически эквивалентно) воспринимать как "идентификатор объекта".
Места хранения типа значения содержат содержимое полей типа значения, но не имеют никакого связанного заголовка. Если код объявляет переменную типа Int32
нет необходимости хранить информацию с этим Int32
говоря, что это такое. Тот факт, что это место содержит Int32
эффективно хранится как часть программы, поэтому его не нужно хранить в самом месте. Это представляет большую экономию, если, например, один имеет миллион объектов, каждый из которых имеет поле типа Int32
, Каждый из объектов, содержащих Int32
имеет заголовок, который идентифицирует класс, который может с ним работать. Поскольку одна копия этого кода класса может работать с любым из миллиона экземпляров, имея тот факт, что поле является Int32
Быть частью кода гораздо эффективнее, чем хранение в каждом из этих полей информации о том, что это такое.
Упаковка необходима, когда делается запрос на передачу содержимого места хранения типа значения в код, который не знает, ожидать этого конкретного типа значения. Код, который ожидает объекты неизвестного типа, может принять ссылку на объект, хранящийся в куче. Поскольку каждый объект, хранящийся в куче, имеет заголовок, определяющий, какой это тип объекта, код может использовать этот заголовок всякий раз, когда необходимо использовать объект таким способом, который потребует знания его типа.
Обратите внимание, что в.net можно объявить так называемые универсальные классы и методы. Каждое такое объявление автоматически генерирует семейство классов или методов, которые идентичны, за исключением того типа объекта, на который они рассчитывают воздействовать. Если кто-то проходит Int32
к рутине DoSomething<T>(T param)
, это автоматически сгенерирует версию подпрограммы, в которой каждый экземпляр типа T
эффективно заменяется Int32
, Эта версия подпрограммы будет знать, что каждое хранилище объявлено как тип T
держит Int32
так же, как в случае, когда подпрограмма была жестко запрограммирована для использования Int32
хранилище, нет необходимости хранить информацию о типе вместе с этими местоположениями.
Я считаю, что это также потому, что примитивы не наследуются от объекта. Предположим, у вас есть метод, который хочет иметь возможность принимать что-либо вообще в качестве параметра, например.
class Printer {
public void print(Object o) {
...
}
}
Вам может понадобиться передать простое примитивное значение этому методу, например:
printer.print(5);
Вы могли бы сделать это без упаковки / распаковки, потому что 5 является примитивом, а не объектом. Вы могли бы перегрузить метод печати для каждого примитивного типа, чтобы включить такую функциональность, но это боль.
Я могу только сказать вам для Java, почему он не поддерживает типы примитивов в дженериках.
Сначала возникла проблема, заключающаяся в том, что вопрос о том, чтобы поддерживать это каждый раз, вызывал дискуссию о том, должны ли Java иметь примитивные типы. Что, конечно, мешало обсуждению актуального вопроса.
Во-вторых, главная причина не включать это было в том, что они хотели двоичной обратной совместимости, чтобы она работала без изменений на виртуальной машине, не зная об обобщениях. Эта причина обратной совместимости / миграции совместимости также является причиной того, что теперь API-интерфейс Collections поддерживает дженерики и остался прежним, и нет (как в C#, когда они ввели дженерики) полного нового набора универсального API-интерфейса Collection.
Совместимость была достигнута с использованием ersure (общая информация о параметрах типа, удаленная во время компиляции), что также является причиной того, что вы получаете так много непроверенных предупреждений о приведении типов в Java.
Вы все еще можете добавить переработанные дженерики, но это не так просто. Простое добавление типа info add runtime вместо удаления не будет работать, так как это нарушает совместимость с исходным кодом и двоичную версию (вы не можете продолжать использовать необработанные типы и не можете вызывать существующий скомпилированный код, потому что у них нет соответствующих методов).
Другой подход - тот, который выбрал C#: см. Выше
И автоматическое автоматическое создание / распаковка не было поддержано для этого варианта использования, потому что автоматическое размещение стоит слишком дорого.
В Java и C# (в отличие от C++) все расширяет Object, поэтому классы коллекций, такие как ArrayList, могут содержать Object или любого из его потомков (в основном что угодно).
Однако из соображений производительности примитивам в Java или типам значений в C# был присвоен особый статус. Они не объект. Вы не можете сделать что-то вроде (в Java):
7.toString()
Хотя toString - это метод для Object. Для того, чтобы связать этот узел с производительностью, были созданы эквивалентные объекты. AutoBoxing удаляет стандартный код необходимости вставлять примитив в его класс-обертку и извлекать его снова, делая код более читабельным.
Разница между типами значений и объектами в C# более серая. Смотрите здесь о том, как они отличаются.