Зачем нам нужен бокс и распаковка в C #?

325

Зачем нам нужен бокс и распаковка в C #?

Я знаю, что такое бокс и распаковка, но я не могу понять, как это реально использовать. Почему и где я должен это использовать?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing
Вайбхав Джайн
источник

Ответы:

482

Зачем

Иметь унифицированную систему типов и позволить типам значений иметь совершенно другое представление своих базовых данных от способа, которым ссылочные типы представляют свои базовые данные (например, это intпросто набор из тридцати двух битов, который полностью отличается от эталонного тип).

Думайте об этом так. У вас есть переменная oтипа object. И теперь у вас есть, intи вы хотите положить его в o. oэто ссылка на что-то где-то, и intэто категорически не ссылка на что-то где-то (в конце концов, это просто число). Итак, вы делаете следующее: вы создаете новое, objectкоторое может хранить, intа затем назначаете ссылку на этот объект o. Мы называем этот процесс «боксом».

Таким образом, если вам не нужна единая система типов (т. Е. Ссылочные типы и типы значений имеют очень разные представления и вам не нужен общий способ «представлять» их), вам не нужен бокс. Если вам не важно intпредставлять их базовое значение (т. Е. Вместо этого иметь intтакже ссылочные типы и просто хранить ссылку на их базовое значение), тогда вам не нужен бокс.

где я должен использовать это.

Например, старый тип коллекции ArrayListест только objects. То есть он хранит только ссылки на то, что где-то живет. Без бокса вы не можете положить intв такую ​​коллекцию. Но с боксом вы можете.

Теперь, во времена дженериков, вам это действительно не нужно, и вы, как правило, можете развлекаться, не задумываясь о проблеме. Но есть несколько предостережений, о которых следует знать:

Это верно:

double e = 2.718281828459045;
int ee = (int)e;

Это не:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception

Вместо этого вы должны сделать это:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;

Сначала мы должны явно распаковать double( (double)o), а затем привести его к int.

Что является результатом следующего:

double e = 2.718281828459045;
double d = e;
object o1 = d;
object o2 = e;
Console.WriteLine(d == e);
Console.WriteLine(o1 == o2);

Подумайте об этом на секунду, прежде чем перейти к следующему предложению.

Если бы вы сказали Trueи Falseздорово! Чего ждать? Это потому, что ==в ссылочных типах используется равенство ссылок, которое проверяет, равны ли ссылки, а не равны ли базовые значения. Это опасно легкая ошибка. Возможно, даже более тонкий

double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);

также будет печатать False!

Лучше сказать:

Console.WriteLine(o1.Equals(o2));

который затем, к счастью, напечатает True.

Еще одна тонкость:

[struct|class] Point {
    public int x, y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Point p = new Point(1, 1);
object o = p;
p.x = 2;
Console.WriteLine(((Point)o).x);

Какой выход? Это зависит! Если Pointесть, structто выходной, 1но если Pointесть, classто выходной 2! Конверсионное преобразование создает копию упакованного значения, объясняющего разницу в поведении.

Ясон
источник
@ Джейсон Вы хотите сказать, что если у нас есть примитивные списки, то нет никаких оснований использовать какой-либо бокс / распаковку?
Pacerier
Я не уверен, что вы подразумеваете под "примитивным списком".
Джейсон
3
Не могли бы вы рассказать о влиянии на производительность boxingи unboxing?
Кевин Мередит
@KevinMeredith есть базовое объяснение производительности для операций упаковки
InfZero
2
Отличный ответ - лучше, чем большинство объяснений, которые я читал в уважаемых книгах.
FredM
59

В платформе .NET существует два вида типов - типы значений и ссылочные типы. Это относительно распространено в ОО языках.

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

Теперь, в былые времена (1.0 из Microsoft.NET), не было этого новомодного дженерика хуллабалу. Вы не могли написать метод с одним аргументом, который мог бы обслуживать тип значения и ссылочный тип. Это нарушение полиморфизма. Таким образом, бокс был принят как средство для приведения типа значения в объект.

Если бы это было невозможно, рамки были бы завалены методами и классами, единственной целью которых было принятие других видов типа. И не только это, но так как типы значений не имеют общего предка общего типа, вам придется иметь разную перегрузку метода для каждого типа значения (бит, байт, int16, int32 и т. Д. И т. Д. И т. Д.).

Бокс предотвратил это. И именно поэтому британцы отмечают День подарков.


источник
1
До дженериков, автобокс был необходим, чтобы сделать много вещей; однако, учитывая наличие обобщений, если бы не необходимость поддерживать совместимость со старым кодом, я думаю, что .net будет лучше без подразумеваемых преобразований в бокс. Приведение типа значения как List<string>.Enumeratorк IEnumerator<string>приводит к объекту, который в основном ведет себя как тип класса, но с неработающим Equalsметодом. Лучший способ броска , List<string>.Enumeratorчтобы IEnumerator<string>бы вызвать оператор пользовательского преобразования, но существование подразумеваемого предотвращает конверсионных что.
суперкат
42

Лучший способ понять это - взглянуть на языки программирования более низкого уровня, на которых строится C #.

В языках самого низкого уровня, таких как C, все переменные находятся в одном месте: стек. Каждый раз, когда вы объявляете переменную, она попадает в стек. Они могут быть только примитивными значениями, такими как bool, byte, 32-bit int, 32-bit uint и т. Д. Стек является простым и быстрым. Когда переменные добавляются, они просто идут одна поверх другой, поэтому первое, что вы объявляете, находится, скажем, в 0x00, следующее в 0x01, следующее в 0x02 в ОЗУ и т. Д. Кроме того, переменные часто предварительно адресуются при компиляции. время, поэтому их адрес известен еще до того, как вы запустите программу.

На следующем уровне вверх, как в C ++, вводится вторая структура памяти, которая называется Heap. Вы по-прежнему в основном живете в стеке, но в стек можно добавить специальные целые, называемые указателями , которые хранят адрес памяти для первого байта объекта, и этот объект живет в куче. Куча - это беспорядок и несколько дорогой в обслуживании, потому что в отличие от переменных стека они не накапливаются линейно вверх, а затем вниз при выполнении программы. Они могут приходить и уходить в определенной последовательности, и они могут расти и уменьшаться.

Работать с указателями сложно. Они являются причиной утечек памяти, переполнения буфера и разочарования. C # на помощь.

На более высоком уровне, C #, вам не нужно думать об указателях. Платформа .Net (написанная на C ++) думает об этом для вас и представляет их вам как ссылки на объекты, а для производительности позволяет хранить более простые значения как bools, bytes и int как Типы Значения. Под капотом объекты и вещи, которые создают экземпляр класса, идут в дорогую, управляемую памятью кучу, в то время как типы значений идут в том же стеке, который вы имели в низкоуровневом C - сверхбыстрый.

Для упрощения взаимодействия между этими 2 принципиально различными концепциями памяти (и стратегий хранения) с точки зрения кодировщика, типы значений могут быть упакованы в любое время. Бокс заставляет копировать значение из стека, помещать в объект и помещать в кучу - более дорогое, но плавное взаимодействие с эталонным миром. Как указывают другие ответы, это произойдет, когда вы, например, скажете:

bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!

Яркой иллюстрацией преимущества бокса является проверка на ноль:

if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false

Наш объект o является технически адресом в стеке, который указывает на копию нашего bool b, который был скопирован в кучу. Мы можем проверить на ноль, потому что бул был в штучной упаковке и положил туда.

В общем, вы должны избегать бокса, если вам это не нужно, например, передавать int / bool / what в качестве объекта аргументу. В .Net есть некоторые базовые структуры, которые все еще требуют передачи Типов Значений как объекта (и поэтому требуют Бокса), но по большей части вам никогда не понадобится Box.

Неисчерпывающий список исторических структур C #, требующих бокса, которых следует избегать:

  • Система событий, как оказалось, имеет условие гонки в наивном использовании, и она не поддерживает асинхронность. Добавьте в проблему бокса, и этого, вероятно, следует избегать. (Вы можете заменить его, например, на асинхронную систему событий, которая использует Generics.)

  • Старые модели Threading и Timer принудительно устанавливали Box по своим параметрам, но были заменены на async / await, которые намного чище и эффективнее.

  • Коллекции .Net 1.1 полностью полагались на бокс, потому что они были раньше, чем Generics. Они все еще работают в System.Collections. В любом новом коде вы должны использовать Коллекции из System.Collections.Generic, которые, помимо того, что избегают Бокса, также обеспечат вам большую безопасность типов .

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

Предложение Микаэля ниже:

Сделай это

using System.Collections.Generic;

var employeeCount = 5;
var list = new List<int>(10);

Не этот

using System.Collections;

Int32 employeeCount = 5;
var list = new ArrayList(10);

Обновить

Этот ответ первоначально предполагал, что Int32, Bool и т. Д. Вызывают бокс, хотя на самом деле это простые псевдонимы для типов значений. То есть .Net имеет типы, такие как Bool, Int32, String и C #, их псевдонимы - bool, int, string без какой-либо функциональной разницы.

Крис Москини
источник
4
Вы научили меня тому, что сто программистов и ИТ-профессионалов не могли объяснить в течение многих лет, но измените его, чтобы сказать, что вы должны делать, а не чего избегать, потому что стало немного трудно следовать ... основные правила чаще всего не действуют 1 Вы не должны этого делать, вместо этого сделайте это
Микаэль Пуусаари
2
Этот ответ должен был быть помечен как ОТВЕТ сто раз!
Пуян
3
в c # нет "Int", есть int и Int32. Я полагаю, что вы ошибаетесь, утверждая, что один является типом значения, а другой является ссылочным типом, заключающим в себе тип значения. если я не ошибаюсь, это верно в Java, но не в C #. В C # те, которые отображаются синим цветом в IDE, являются псевдонимами для их определения структуры. Итак: int = Int32, bool = Boolean, string = String. Причина использования bool вместо Boolean заключается в том, что это предлагается в соответствии с рекомендациями и соглашениями по разработке MSDN. В противном случае я люблю этот ответ. Но я буду голосовать до тех пор, пока вы не докажете, что я не прав или исправите это в своем ответе
Хериберто Луго
2
Если вы объявите переменную как int, а другую как Int32, или bool и Boolean - щелкните правой кнопкой мыши и просмотрите определение, в результате вы получите то же определение для структуры.
Хериберто Луго
2
@HeribertoLugo верен, строка «Вам следует избегать объявления типов значений как Bool вместо bool» ошибочна. Как указывает OP, вы должны избегать объявления своего типа bool (или Boolean, или любого другого типа значения) как Object. bool / Boolean, int / Int32, это просто псевдонимы между C # и .NET: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
STW,
21

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

Используя общие коллекции сейчас, это в значительной степени исчезает. Если вы создаете List<int>, бокс не выполняется - он List<int>может содержать целые числа напрямую.

луч
источник
Вам все еще нужен бокс для таких вещей, как составное форматирование строк. Вы можете видеть это не так часто при использовании дженериков, но это определенно все еще там.
Джереми С
1
true - он все время отображается и в ADO.NET - все значения параметров sql являются «объектными» независимо от типа данных
Ray
11

Упаковка и распаковка специально используются для обработки объектов типа значения как ссылочного типа; перемещение их фактического значения в управляемую кучу и доступ к их значению по ссылке.

Без упаковки и распаковки вы никогда не сможете передавать типы значений по ссылке; и это означает, что вы не можете передавать типы значений как экземпляры Object.

STW
источник
по- прежнему хороший ответ после того, как почти 10 лет сэр +1
ОСШ
1
Передача по ссылке на числовые типы существует в языках без упаковки, а другие языки реализуют обработку типов значений как экземпляров объекта без упаковки и перемещение значения в кучу (например, реализации динамических языков, в которых указатели выровнены по 4-байтовым границам, используют нижние четыре биты ссылок для указания того, что значение является целым числом или символом, а не полным объектом; такие типы значений являются неизменяемыми и имеют тот же размер, что и указатель).
Пит Киркхэм
8

Последнее, что мне пришлось распаковать, - это когда я писал код, который извлекал некоторые данные из базы данных (я не использовал LINQ to SQL , просто старый ADO.NET ):

int myIntValue = (int)reader["MyIntValue"];

По сути, если вы работаете с более старыми API, прежде чем генерики, вы столкнетесь с боксом. Кроме этого, это не так часто.

BFree
источник
4

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

Я не думаю, что это правда, попробуйте вместо этого:

class Program
    {
        static void Main(string[] args)
        {
            int x = 4;
            test(x);
        }

        static void test(object o)
        {
            Console.WriteLine(o.ToString());
        }
    }

Это работает очень хорошо, я не использовал бокс / распаковку. (Разве компилятор делает это за кадром?)

Манодж
источник
Это потому, что все унаследовано от System.Object, и вы даете методу объект с дополнительной информацией, поэтому в основном вы вызываете метод test с тем, что он ожидает, и с чем угодно, поскольку он не ожидает ничего конкретного. Многое в .NET делается за кулисами, и причина, по которой этот язык очень прост в использовании
Микаэль Пуусаари
1

В .net каждый экземпляр Object или любой производный от него тип включает структуру данных, которая содержит информацию о его типе. Типы «реальных» значений в .net не содержат никакой такой информации. Чтобы позволить данным в типах значений манипулировать подпрограммами, которые ожидают получения типов, полученных из объекта, система автоматически определяет для каждого типа значения соответствующий тип класса с теми же членами и полями. Бокс создает новые экземпляры этого типа класса, копируя поля из экземпляра типа значения. Распаковка копирует поля из экземпляра типа класса в экземпляр типа значения. Все типы классов, которые создаются из типов значений, являются производными от иронически названного класса ValueType (который, несмотря на свое имя, на самом деле является ссылочным типом).

Supercat
источник
0

Когда метод принимает в качестве параметра только ссылочный тип (скажем, универсальный метод, ограниченный как класс через newограничение), вы не сможете передать ему ссылочный тип и поставить его в рамку.

Это также верно для любых методов , которые принимают в objectкачестве параметра - это будет иметь , чтобы быть ссылочного типа.

Одед
источник
0

В общем, вы, как правило, хотите избегать упаковки типов значений.

Тем не менее, есть редкие случаи, где это полезно. Например, если вам нужно ориентироваться на платформу 1.1, у вас не будет доступа к универсальным коллекциям. Любое использование коллекций в .NET 1.1 потребует обработки вашего типа значения как System.Object, что приведет к упаковке / распаковке.

Есть еще случаи, когда это может быть полезно в .NET 2.0+. В любое время, когда вы хотите воспользоваться тем фактом, что все типы, включая типы значений, могут рассматриваться как объект напрямую, вам может понадобиться использовать упаковку / распаковку. Иногда это может быть удобно, поскольку позволяет сохранять любой тип в коллекции (используя объект вместо T в универсальной коллекции), но в целом этого лучше избегать, поскольку вы теряете безопасность типов. Тем не менее, один из случаев, когда часто происходит боксирование, - это использование Reflection. Многие вызовы в отражении требуют бокса / распаковки при работе с типами значений, так как тип заранее неизвестен.

Hunain
источник
0

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

Что касается того, что на самом деле делает бокс. вот несколько примеров

Mono C ++

void* mono_object_unbox (MonoObject *obj)
 {    
MONO_EXTERNAL_ONLY_GC_UNSAFE (void*, mono_object_unbox_internal (obj));
 }

#define MONO_EXTERNAL_ONLY_GC_UNSAFE(t, expr) \
    t result;       \
    MONO_ENTER_GC_UNSAFE;   \
    result = expr;      \
    MONO_EXIT_GC_UNSAFE;    \
    return result;

static inline gpointer
mono_object_get_data (MonoObject *o)
{
    return (guint8*)o + MONO_ABI_SIZEOF (MonoObject);
}

#define MONO_ABI_SIZEOF(type) (MONO_STRUCT_SIZE (type))
#define MONO_STRUCT_SIZE(struct) MONO_SIZEOF_ ## struct
#define MONO_SIZEOF_MonoObject (2 * MONO_SIZEOF_gpointer)

typedef struct {
    MonoVTable *vtable;
    MonoThreadsSync *synchronisation;
} MonoObject;

Распаковка в Mono - это процесс приведения указателя со смещением в 2 gpointers в объекте (например, 16 байтов). А gpointerэто void*. Это имеет смысл, если смотреть на определение, MonoObjectпоскольку это явно просто заголовок для данных.

C ++

Чтобы поместить значение в C ++, вы можете сделать что-то вроде:

#include <iostream>
#define Object void*

template<class T> Object box(T j){
  return new T(j);
}

template<class T> T unbox(Object j){
  T temp = *(T*)j;
  delete j;
  return temp;
}

int main() {
  int j=2;
  Object o = box(j);
  int k = unbox<int>(o);
  std::cout << k;
}
Льюис Келси
источник