Для чого нам потрібен бокс і розпакування в C #?


323

Для чого нам потрібен бокс і розпакування в C #?

Я знаю, що таке бокс та unboxing, але я не можу зрозуміти реальне використання цього. Чому і де я повинен його використовувати?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing

Відповіді:


480

Чому?

Мати єдину систему типів і дозволяти типам значень мати зовсім інше представлення їх базових даних, так як референтні типи представляють їх базові дані (наприклад, an 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! Перетворення боксу робить копію значення, яке міститься в коробці, пояснюючи різницю в поведінці.


@Jason Ви хочете сказати, що якщо у нас є примітивні списки, немає причин використовувати будь-який бокс / розпакування?
Pacerier

Я не впевнений, що ви маєте на увазі під "примітивним списком".
Jason

3
Не могли б ви поговорити з впливом на ефективність boxingта unboxing?
Кевін Мередіт

@KevinMeredith є основне пояснення щодо продуктивності боксерських та розблокування
InfZero

2
Відмінна відповідь - краще, ніж більшість пояснень, які я читав у добре розцінених книгах.
FredM

58

У рамках .NET є два види типів - типи значень та типи посилань. Це відносно часто в мовах ОО.

Однією з важливих особливостей об'єктно-орієнтованих мов є можливість обробляти екземпляри типово-агностичним способом. Це називається поліморфізмом . Оскільки ми хочемо скористатись поліморфізмом, але у нас є два різних види, то має бути певний спосіб їх об’єднання, щоб ми могли поводитися з тим чи іншим тим самим.

Тепер, ще за старих часів (1.0 Microsoft.NET), не було цього новомодного генеричного джуллабалу. Ви не можете написати метод, який мав єдиний аргумент, який може обслуговувати тип значення та тип посилання. Це порушення поліморфізму. Так бокс був прийнятий як засіб примусового значення типу в об'єкт.

Якби це було неможливо, рамки були б засмічені методами та класами, єдиною метою яких було прийняття інших видів типу. Мало того, але оскільки типи значень справді не мають спільного предка загального типу, вам доведеться мати перевантаження різних методів для кожного типу значень (біт, байт, int16, int32, тощо, тощо, тощо).

Бокс заважав цьому не статися. І тому англійці відзначають День боксу.


1
Перед дженериками автобоксу потрібно було робити багато речей; однак, враховуючи існування генерики, якби не потреба підтримувати сумісність зі старим кодом, я думаю. Виливки типу значення , як List<string>.Enumeratorз IEnumerator<string>виходами об'єкта , який в основному веде себе як тип класу, але з розбитим Equalsметодом. Кращий спосіб кидка , List<string>.Enumeratorщоб IEnumerator<string>б викликати оператор призначеного для користувача перетворення, але існування якого мається на увазі запобігає конверсійних що.
supercat

42

Найкращий спосіб зрозуміти це - подивитися на мови програмування нижчого рівня, на яких працює C #.

У мовах найнижчого рівня, таких як C, всі змінні переходять на одне місце: Стек. Кожен раз, коли ви оголошуєте змінну, вона переходить у стек. Вони можуть бути лише примітивними значеннями, такими як bool, байт, 32-бітний int, 32-бітова uint тощо. Стек є простим і швидким. Як додані змінні, вони просто переходять один на інший, тому перший, який ви оголошуєте, сидить у скажімо, 0x00, наступний у 0x01, наступний у 0x02 в оперативній пам’яті тощо. Крім того, змінні часто попередньо розглядаються під час компіляції- час, тому їх адреса відома ще до того, як ви навіть запустите програму.

На наступному рівні вгору, як C ++, вводиться друга структура пам'яті під назвою Heap. Ви все ще живете в Стек, але в стек можна додати спеціальні вставки під назвою Покажчики , які зберігають адресу пам'яті для першого байта Об'єкта, і що Об'єкт живе в Купі. Купа - це безлад і дещо дорого в обслуговуванні, тому що на відміну від змінних Stack вони не збираються лінійно вгору, а потім вниз при виконанні програми. Вони можуть приходити і виходити без певної послідовності, і вони можуть рости і скорочуватися.

Справлятися з покажчиками важко. Вони є причиною протікання пам’яті, перевиконання буфера та розладу. C # на допомогу.

На більш високому рівні, C #, вам не потрібно думати про вказівники - .Net фреймворк (написаний на C ++) думає про це для вас і представляє їх вам як Посилання на об'єкти, а для продуктивності дозволяє зберігати більш прості значення як булі, байти та ints як типи значень. Під кришкою об'єкти та речі, які створюють клас, надходять на дорогу кучу, керовану пам’яттю, тоді як типи значень переходять у той самий стек, який у вас був на низькому рівні С - надшвидкий.

З метою збереження взаємодії між цими двома принципово різними концептами пам'яті (та стратегіями зберігання) простою з точки зору кодера, типи значень можуть бути розміщені в будь-якому часі. Бокс призводить до того, що значення буде скопійовано зі Стек, поміщено в Об'єкт та розміщено на Купі - дорожча, але, взаємодія з рідиною з Референтним світом. Як вказують інші відповіді, це станеться, коли ви, наприклад, скажете:

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, яка була скопійована в Купу. Ми можемо перевірити o на null, оскільки bool був Boxed і поміщений туди.

Як правило, вам слід уникати боксу, якщо він вам не потрібен, наприклад, щоб передавати аргумент int / bool / що завгодно як об'єкт. Є деякі основні структури в .Net, які все ще вимагають передавати Типи значень як об'єкт (і так вимагає Boxing), але здебільшого вам ніколи не потрібно буде Box.

Невичерпний перелік історичних структур C #, які потребують боксу, яких слід уникати:

  • Виявляється, система Event має стан перегонів у наївному використанні, і вона не підтримує асинхронізацію. Додайте до проблеми боксу, і, мабуть, цього слід уникати. (Ви можете замінити його, наприклад, системою подій async, яка використовує Generics.)

  • Старі моделі Threading і Timer вимушували Box за своїми параметрами, але були замінені асинхронізуванням / очікуванням, які набагато чистіші та ефективніші.

  • Колекції .Net 1.1 повністю покладалися на Boxing, тому що вони вийшли до Generics. Вони все ще б’ються навколо у System.Collections. У будь-якому новому коді ви повинні використовувати колекції з System.Collections.Generic, які крім уникнення боксу також забезпечують більш високу безпеку типу .

Вам слід уникати декларування або передачі Ваших типів цінності як об'єктів, якщо вам не доведеться мати справу з вищезазначеними історичними проблемами, які викликають бокс, і ви хочете уникнути удару Boxing про ефективність пізніше, коли ви знаєте, що він буде в будь-якому випадку бокс.

За пропозицією Мікаеля нижче:

Зробити це

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 тощо викликати бокс, адже насправді вони є простими псевдонімами для Value Types. Тобто .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" помиляється. Як зазначає ОП, ви повинні уникати оголошення bool (або булевого або будь-якого іншого типу значень) як Object. bool / Boolean, int / Int32 - лише псевдоніми між C # і .NET: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
STW

21

Бокс насправді не те, що ви використовуєте - це те, що виконуються під час виконання, щоб ви могли обробляти посилання та типи значень однаково, коли це необхідно. Наприклад, якщо ви використовували ArrayList для утримання списку цілих чисел, цілі числа отримали коробку, щоб поміститися в слоти об'єктного типу в ArrayList.

Використовуючи загальні колекції зараз, це майже все минає. Якщо ви створюєте List<int>, бокс не робиться - List<int>цілі числа можуть містити безпосередньо цілі числа.


Вам все ще потрібен бокс для таких речей, як композитне форматування рядків. Ви можете бачити його не так часто, коли використовуєте дженерики, але це, безумовно, все ще є.
Jeremy S

1
правда , - він показує весь час в ADO.NET теж - SQL значення параметрів все «об'єкта незалежно від того , що реальний тип даних
Ray

11

Boxing і Unboxing спеціально використовуються для обліку об'єктів типу значень як довідкового типу; переміщення їх фактичного значення до керованої купи та доступ до їх значення за допомогою посилання.

Без боксу та розпакування ви ніколи не можете передавати типи значень за посиланням; а це означає, що ви не можете передавати типи значень як екземпляри Object.


все ще приємна відповідь через майже 10 років сер +1
snr

1
Передача посилань на числові типи існує в мовах без боксу, а інші мови реалізують трактування типів значень як екземпляри Об'єкта без боксу та переміщення значення в купу (наприклад, реалізація динамічних мов, де покажчики вирівнюються до 4-х байтних меж, використовують нижню чотири біти посилань, які вказують на те, що значення є цілим чи символом, а не повноцінним об'єктом; такі типи значень є незмінними і мають такий же розмір, як вказівник).
Піт Кіркхем

8

Останнє місце, за яким я мав щось розблокувати, коли писав якийсь код, який отримував деякі дані з бази даних (я не використовував LINQ в SQL , просто звичайний старий ADO.NET ):

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

В основному, якщо ви працюєте зі старими API-програмами перед дженериками, ви зіткнетеся з боксом. Крім цього, це не так часто.


4

Бокс необхідний, коли у нас є функція, яка потребує об'єкта як параметр, але у нас є різні типи значень, які потрібно передавати, у цьому випадку нам потрібно спочатку перетворити типи значень у типи даних об'єкта, перш ніж передати його у функцію.

Я не думаю, що це правда, спробуйте це:

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

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

Це працює чудово, я не використовував бокс / розпакування. (Якщо компілятор не робить це за кадром?)


Це тому, що все успадковується від System.Object, і ви надаєте методу об'єкт із додатковою інформацією, тому в основному ви викликаєте метод тестування з тим, чого він очікує, і всього, чого він може очікувати, оскільки він нічого не чекає, зокрема. Багато в .NET робиться за лаштунками, і причина, чому це дуже проста мова,
Мікаель Пуусаарі

1

У .net кожен екземпляр Об'єкта або будь-якого типу, похідного від нього, включає структуру даних, яка містить інформацію про його тип. "Реальні" типи значень у .net не містять такої інформації. Щоб дозволити маніпулювати даними у типах значень підпрограми, які очікують отримання типів, похідних від об'єкта, система автоматично визначає для кожного типу значень відповідний тип класу з тими ж членами та полями. Бокс створює нові екземпляри цього типу класу, копіюючи поля з екземпляра типу значення. Розв’язування копіює поля з екземпляра типу класу в екземпляр типу значення. Усі типи класів, створені з типів значень, походять від іронічно названого класу ValueType (який, незважаючи на свою назву, насправді є еталонним типом).


0

Якщо метод приймає лише параметр типу посилань (скажімо, загальний метод, обмежений класом через newобмеження), ви не зможете передавати йому тип посилання і доведеться ввести його в поле.

Це справедливо і для будь-яких методів, які приймаються objectза параметр - це повинен бути еталонним типом.


0

Як правило, ви зазвичай хочете уникати боксу ваших типів цінності.

Однак трапляються рідкісні випадки, коли це корисно. Якщо вам потрібно націлити орієнтацію на 1.1 рамку, ви не матимете доступу до загальних колекцій. Будь-яке використання колекцій у .NET 1.1 потребує трактування типу значень як System.Object, що спричиняє бокс / розпакування.

Ще є випадки, коли це може бути корисним у .NET 2.0+. Кожен раз, коли ви хочете скористатися тим, що всі типи, включаючи типи значень, можна розглядати як об'єкт безпосередньо, можливо, вам доведеться використовувати бокс / розпакування. Це може бути зручно часом, оскільки це дозволяє зберегти будь-який тип у колекції (використовуючи об’єкт замість T у загальній колекції), але в цілому краще цього уникати, оскільки ви втрачаєте безпеку типу. Однак один із випадків, коли бокс часто трапляється, коли ви використовуєте Reflection - для багатьох дзвінків, що відображаються, знадобиться бокс / розблокування під час роботи зі значеннями типів, оскільки тип не відомий заздалегідь.


0

Бокс - це перетворення значення в типовий тип з даними при деякому зміщенні в об'єкті на купі.

Що стосується того, чим насправді займається бокс. Ось кілька прикладів

Моно С ++

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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.