Dlaczego potrzebujemy boksowania i rozpakowywania w C #?


323

Dlaczego potrzebujemy boksowania i rozpakowywania w C #?

Wiem, co to jest boksowanie i rozpakowywanie, ale nie potrafię pojąć jego prawdziwego wykorzystania. Dlaczego i gdzie mam go używać?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing

Odpowiedzi:


480

Dlaczego

Aby mieć zunifikowany system typów i pozwolić, aby typy wartości miały zupełnie inną reprezentację swoich bazowych danych niż sposób, w jaki typy referencyjne reprezentują ich bazowe dane (np. A intto tylko koszyk trzydziestu dwóch bitów, który jest całkowicie inny niż referencja rodzaj).

Pomyśl o tym w ten sposób. Masz zmienną otypu object. A teraz masz inti chcesz to włożyć o. ojest odniesieniem do czegoś, a intzdecydowanie nie jest odniesieniem do czegoś (w końcu to tylko liczba). Tak więc robisz to: tworzysz nowy, objectktóry może przechowywać, inta następnie przypisujesz odwołanie do tego obiektu o. Ten proces nazywamy „boksowaniem”.

Jeśli więc nie zależy ci na zunifikowanym systemie typów (tj. Typy referencyjne i typy wartości mają bardzo różne reprezentacje i nie chcesz wspólnego sposobu „reprezentowania” tych dwóch), nie potrzebujesz boksu. Jeśli nie zależy ci na intreprezentowaniu ich wartości bazowej (tj. Zamiast tego intsą też typami referencyjnymi i po prostu przechowują referencje do ich wartości bazowej), nie potrzebujesz boksu.

gdzie powinienem go użyć.

Na przykład stary typ kolekcji ArrayListzjada tylko objects. Oznacza to, że przechowuje tylko odniesienia do czegoś, co gdzieś mieszka. Bez boksu nie można umieścić inttakiej kolekcji. Ale w boksie możesz.

Teraz, w czasach ogólnych, tak naprawdę nie jest to potrzebne i ogólnie można wesoło iść bez zastanowienia się nad problemem. Ale należy pamiętać o kilku zastrzeżeniach:

To jest poprawne:

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

To nie jest:

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

Zamiast tego musisz to zrobić:

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

Najpierw musimy jawnie rozpakować double( (double)o), a następnie przesłać to do pliku int.

Co jest wynikiem:

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

Pomyśl o tym przez chwilę, zanim przejdziesz do następnego zdania.

Jeśli powiedziałeś Truei Falseświetnie! Czekaj, co? Jest tak, ponieważ ==w przypadku typów referencyjnych używa się równości referencyjnej, która sprawdza, czy referencje są równe, a nie czy podstawowe wartości są równe. Jest to niebezpiecznie łatwy do popełnienia błąd. Może nawet bardziej subtelny

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

wydrukuje również False!

Lepiej powiedzieć:

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

który na szczęście wydrukuje True.

Ostatnia subtelność:

[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);

Jaka jest wydajność? To zależy! Jeśli Pointjest a, structto wyjście jest, 1ale jeśli Pointjest, to classwyjście jest 2! Konwersja boksu tworzy kopię wartości w pudełku, wyjaśniając różnicę w zachowaniu.


@Jason Czy chcesz powiedzieć, że jeśli mamy prymitywne listy, nie ma powodu, aby używać boksu / rozpakowania?
Pacerier

Nie jestem pewien, co rozumiesz przez „prymitywną listę”.
jason

3
Czy możesz porozmawiać o wpływie na boxingi unboxing?
Kevin Meredith,


2
Doskonała odpowiedź - lepsza niż większość wyjaśnień, które czytałem w dobrze ocenianych książkach.
FredM

58

W systemie .NET istnieją dwa rodzaje typów - typy wartości i typy referencyjne. Jest to stosunkowo powszechne w językach OO.

Jedną z ważnych cech języków obiektowych jest umiejętność obsługi instancji w sposób niezależny od typu. Jest to określane jako polimorfizm . Ponieważ chcemy skorzystać z polimorfizmu, ale mamy dwa różne gatunki typów, musi istnieć sposób ich połączenia, abyśmy mogli poradzić sobie z jednym lub drugim w ten sam sposób.

Teraz, w dawnych czasach (1.0 Microsoft.NET), nie było tego nowo opracowanego generycznego hullabaloo. Nie można napisać metody, która ma jeden argument, który mógłby obsłużyć typ wartości i typ odwołania. To naruszenie polimorfizmu. Tak więc boks został przyjęty jako sposób na przymuszenie typu wartości do obiektu.

Gdyby to nie było możliwe, ramy byłyby zaśmiecone metodami i klasami, których jedynym celem było zaakceptowanie innego gatunku. Nie tylko to, ale ponieważ typy wartości tak naprawdę nie mają wspólnego przodka typu, musiałbyś mieć inne przeciążenie metod dla każdego typu wartości (bit, bajt, int16, int32 itp. Itd.).

Boks temu zapobiegał. I dlatego Brytyjczycy świętują drugi dzień świąt.


1
Przed lekami generycznymi auto-boxowanie było konieczne do zrobienia wielu rzeczy; Biorąc jednak pod uwagę istnienie generyków, gdyby nie konieczność zachowania zgodności ze starym kodem, myślę, że .net byłby lepszy bez domniemanych konwersji bokserskich. Rzutowanie typu wartości, takiego jak List<string>.Enumeratorna, IEnumerator<string>daje obiekt, który w większości zachowuje się jak typ klasy, ale z uszkodzoną Equalsmetodą. Lepszym sposobem oddanych List<string>.Enumeratordo IEnumerator<string>byłoby wezwać operatora niestandardowych konwersji, ale istnienie domniemanych zapobiega konwersji tego.
supercat

42

Najlepszym sposobem na zrozumienie tego jest spojrzenie na języki programowania niższego poziomu, na których opiera się C #.

W językach najniższego poziomu, takich jak C, wszystkie zmienne idą w jednym miejscu: stosie. Za każdym razem, gdy deklarujesz zmienną, przechodzi ona na stos. Mogą to być tylko prymitywne wartości, takie jak bool, bajt, 32-bitowy int, 32-bitowy uint itp. Stos jest zarówno prosty, jak i szybki. Gdy dodawane są zmienne, po prostu idą jedna na drugą, więc pierwsza deklarowana wartość to 0x00, kolejna 0x01, kolejna 0x02 w pamięci RAM itp. Ponadto zmienne są często wstępnie adresowane podczas kompilacji czas, więc ich adres jest znany jeszcze przed uruchomieniem programu.

Na wyższym poziomie, podobnie jak C ++, wprowadzono drugą strukturę pamięci o nazwie Heap. Nadal w większości mieszkasz na stosie, ale do stosu można dodać specjalne intryny o nazwie Wskaźniki , które przechowują adres pamięci dla pierwszego bajtu obiektu, i ten obiekt żyje na stosie. Sterta jest rodzajem bałaganu i nieco kosztowna w utrzymaniu, ponieważ w przeciwieństwie do zmiennych stosu, nie nakładają się one liniowo w górę, a następnie w dół podczas wykonywania programu. Mogą przychodzić i odchodzić w określonej kolejności, mogą rosnąć i kurczyć się.

Radzenie sobie ze wskaźnikami jest trudne. Są przyczyną wycieków pamięci, przepełnienia bufora i frustracji. C # na ratunek.

Na wyższym poziomie, C #, nie musisz myśleć o wskaźnikach - środowisko .Net (napisane w C ++) myśli o nich dla Ciebie i przedstawia je jako Odwołania do obiektów, a dla wydajności pozwala przechowywać prostsze wartości jak boole, bajty i liczby całkowite jako typy wartości. Pod maską obiekty i rzeczy, które tworzą klasę, trafiają na kosztowne, zarządzane przez pamięć stosy, podczas gdy typy wartości mieszczą się w tym samym stosie, który posiadałeś w C na niskim poziomie - superszybko.

Aby zachować prostotę interakcji między tymi 2 zasadniczo różnymi koncepcjami pamięci (i strategiami przechowywania) z punktu widzenia kodera, typy wartości można w dowolnym momencie łączyć. Boks powoduje, że wartość jest kopiowana ze stosu, umieszczana w obiekcie i umieszczana na stosie - droższa, ale płynna interakcja ze światem odniesienia. Jak wskazują inne odpowiedzi, nastąpi to, gdy na przykład powiesz:

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

Mocną ilustracją przewagi boksu jest sprawdzenie wartości zerowej:

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

Nasz obiekt o jest technicznie adresem w stosie, który wskazuje na kopię naszego bool b, który został skopiowany na stos. Możemy sprawdzić o dla wartości zerowej, ponieważ bool został zapakowany i tam umieszczony.

Ogólnie rzecz biorąc, powinieneś unikać Boksowania, chyba że jest to potrzebne, na przykład, aby przekazać argument do int / bool / cokolwiek innego. Istnieje kilka podstawowych struktur w .Net, które wciąż wymagają przekazywania Typów Wartości jako obiektów (i dlatego wymagają Boksowania), ale w większości przypadków nigdy nie powinieneś potrzebować Boksowania.

Niewyczerpująca lista historycznych struktur C # wymagających boksu, których należy unikać:

  • Okazuje się, że system wydarzeń ma naiwny warunek wyścigu i nie obsługuje asynchronizacji. Dodaj problem boksu i prawdopodobnie należy go unikać. (Możesz go zastąpić na przykład systemem zdarzeń asynchronicznych, który korzysta z Generics).

  • Stare modele wątków i timerów wymuszały na swoich parametrach pole, ale zostały zastąpione przez asynchroniczne / oczekujące, które są znacznie czystsze i wydajniejsze.

  • Kolekcje .Net 1.1 opierały się całkowicie na boksie, ponieważ pojawiły się przed Generics. Te wciąż się pojawiają w System.Collections. W każdym nowym kodzie powinieneś używać kolekcji z System.Collections.Generic, która oprócz unikania boksu zapewnia również większe bezpieczeństwo typu .

Powinieneś unikać deklarowania lub przekazywania Typów Wartości jako obiektów, chyba że musisz poradzić sobie z powyższymi problemami historycznymi, które wymuszają Boks, i chcesz uniknąć późniejszego spadku wydajności Boksu, gdy będziesz wiedział, że i tak zostanie Boxowany.

Zgodnie z sugestią Mikaela poniżej:

Zrób to

using System.Collections.Generic;

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

Nie to

using System.Collections;

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

Aktualizacja

Ta odpowiedź początkowo sugerowała, że ​​Int32, Bool itp. Powodują boksowanie, podczas gdy w rzeczywistości są to proste aliasy dla typów wartości. Oznacza to, że .Net ma typy takie jak Bool, Int32, String i C # aliasy do bool, int, string, bez żadnej różnicy funkcjonalnej.


4
Nauczyłeś mnie tego, czego setka programistów i specjalistów IT nie może wyjaśnić od lat, ale zmień to, aby powiedzieć, co powinieneś robić zamiast tego, czego należy unikać, ponieważ trochę trudno było przestrzegać. Zasady podstawowe najczęściej nie idą 1 Nie powinieneś tego robić, zamiast tego
Mikael Puusaari,

2
Ta odpowiedź powinna była zostać oznaczona jako ODPOWIEDŹ sto razy!
Pouyan

3
nie ma „Int” w c #, jest int i Int32. uważam, że niesłusznie stwierdzasz, że jeden jest typem wartości, a drugi jest typem referencyjnym obejmującym typ wartości. chyba że się mylę, to prawda w Javie, ale nie w C #. W języku C # te, które pokazują IDE w kolorze niebieskim, to aliasy dla ich definicji struktury. Więc: int = Int32, bool = Boolean, string = String. Powodem używania bool zamiast Boolean jest to, że jest tak sugerowane w wytycznych i konwencjach dotyczących projektowania MSDN. W przeciwnym razie uwielbiam tę odpowiedź. Ale będę głosować, dopóki nie udowodnisz, że się mylę lub nie naprawisz tego w swojej odpowiedzi.
Heriberto Lugo

2
Jeśli zadeklarujesz zmienną jako int, a inną jako Int32 lub bool i Boolean - kliknij prawym przyciskiem myszy i wyświetl definicję, skończysz w tej samej definicji dla struktury.
Heriberto Lugo

2
@HeribertoLugo jest poprawny, wiersz „Powinieneś unikać deklarowania typów wartości jako wartości boolowej zamiast boolowej” jest błędny. Jak wskazuje OP, należy unikać deklarowania wartości logicznej (boolowskiej lub innego typu wartości) jako Object. bool / Boolean, int / Int32, to tylko aliasy między C # i .NET: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
STW

21

Boks nie jest tak naprawdę czymś, z czego korzystasz - jest to coś, co używa środowisko wykonawcze, dzięki czemu możesz obsługiwać typy referencji i wartości w ten sam sposób, gdy jest to konieczne. Na przykład, jeśli użyłeś ArrayList do przechowywania listy liczb całkowitych, liczby całkowite zostaną zapakowane, aby zmieściły się w szczelinach typu obiektowego w ArrayList.

Używając teraz ogólnych kolekcji, to prawie znika. Jeśli utworzysz a List<int>, boks nie jest wykonywany - List<int>może on przechowywać liczby całkowite bezpośrednio.


Nadal potrzebujesz boksu do takich rzeczy, jak formatowanie ciągów złożonych. Możesz nie widzieć go tak często, gdy używasz leków generycznych, ale na pewno nadal tam jest.
Jeremy S

1
prawda - pokazuje się cały czas również w ADO.NET - wszystkie wartości parametrów sql są „obiektami bez względu na rzeczywisty typ danych”
Ray

11

Boksowanie i rozpakowywanie są szczególnie używane do traktowania obiektów typu wartości jako typu odniesienia; przenosząc ich rzeczywistą wartość na zarządzaną stertę i uzyskując dostęp do ich wartości przez odniesienie.

Bez boksu i rozpakowania nigdy nie można przekazywać typów wartości przez odniesienie; a to oznacza, że ​​nie można przekazywać typów wartości jako instancji Object.


wciąż miła odpowiedź po prawie 10 latach, proszę pana +1
snr

1
Przekazywanie przez odniesienie typów liczbowych istnieje w językach bez boksu, a inne języki implementują traktowanie typów wartości jako instancji obiektu bez boksu i przenoszenie wartości na stos (np. Implementacje języków dynamicznych, w których wskaźniki są wyrównane do granic 4 bajtów, używają czterech niższych fragmenty odniesień wskazujące, że wartość jest liczbą całkowitą lub symbolem zamiast pełnego obiektu; takie typy wartości są niezmienne i mają taki sam rozmiar jak wskaźnik).
Pete Kirkham

8

Ostatnim miejscem, w którym musiałem coś rozpakować, było pisanie kodu pobierającego niektóre dane z bazy danych (nie korzystałem z LINQ to SQL , tylko zwykły stary ADO.NET ):

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

Zasadniczo, jeśli pracujesz ze starszymi interfejsami API przed generycznymi, napotkasz boks. Poza tym nie jest to takie powszechne.


4

Boks jest wymagany, gdy mamy funkcję, która potrzebuje obiektu jako parametru, ale mamy różne typy wartości, które należy przekazać, w takim przypadku musimy najpierw przekonwertować typy wartości na typy danych obiektu, zanim przekażemy je do funkcji.

Nie sądzę, że to prawda, spróbuj tego zamiast tego:

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

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

To działa dobrze, nie używałem boxowania / rozpakowywania. (Chyba że kompilator robi to za kulisami?)


Dzieje się tak, ponieważ wszystko dziedziczy po System.Object, a ty dajesz metodzie obiekt z dodatkowymi informacjami, więc w zasadzie wywołujesz metodę testową z tym, czego się spodziewa i czymkolwiek może się spodziewać, ponieważ nie oczekuje niczego konkretnego. Wiele w .NET robi się za kulisami, a powód, dla którego jest to bardzo prosty język
Mikael Puusaari

1

W .net każda instancja Object lub dowolnego pochodnego typu zawiera strukturę danych, która zawiera informacje o jej typie. „Prawdziwe” typy wartości w .net nie zawierają takich informacji. Aby umożliwić manipulowanie danymi w typach wartości za pomocą procedur, które oczekują odbierania typów pochodzących od obiektu, system automatycznie określa dla każdego typu wartości odpowiedni typ klasy z tymi samymi elementami i polami. Boks tworzy nowe wystąpienia tego typu klasy, kopiując pola z wystąpienia typu wartości. Rozpakowanie powoduje skopiowanie pól z instancji typu klasy do instancji typu wartości. Wszystkie typy klas, które są tworzone z typów wartości, pochodzą z ironicznie nazwanej klasy ValueType (która pomimo swojej nazwy jest w rzeczywistości typem referencyjnym).


0

Gdy metoda przyjmuje tylko typ referencyjny jako parametr (powiedzmy, że metoda ogólna jest ograniczona przez klasę poprzez newograniczenie), nie będzie można przekazać do niego typu referencyjnego i trzeba go zaznaczyć.

Dotyczy to również wszelkich metod, które przyjmują objectjako parametr - będzie to musiał być typ referencyjny.


0

Ogólnie rzecz biorąc, zazwyczaj będziesz chciał uniknąć boksowania swoich typów wartości.

Są jednak rzadkie przypadki, w których jest to przydatne. Na przykład, jeśli chcesz celować w środowisko 1.1, nie będziesz mieć dostępu do ogólnych zbiorów. Każde użycie kolekcji w .NET 1.1 wymagałoby traktowania twojego typu wartości jako System.Object, co powoduje boksowanie / rozpakowywanie.

Nadal istnieją przypadki, aby było to przydatne w .NET 2.0+. Za każdym razem, gdy chcesz skorzystać z faktu, że wszystkie typy, w tym typy wartości, mogą być traktowane bezpośrednio jako obiekt, może być konieczne użycie boksu / rozpakowania. Może to być czasami przydatne, ponieważ pozwala zapisać dowolny typ w kolekcji (używając obiektu zamiast T w ogólnej kolekcji), ale ogólnie lepiej tego unikać, ponieważ tracisz bezpieczeństwo typu. Jedynym przypadkiem, w którym często pojawia się boks, jest użycie Odbicia - wiele wywołań w odbiciu będzie wymagać boksowania / rozpakowywania podczas pracy z typami wartości, ponieważ ten typ nie jest wcześniej znany.


0

Boks to konwersja wartości na typ odniesienia z danymi z pewnym przesunięciem w obiekcie na stercie.

Co do tego, co faktycznie robi boks. Oto kilka przykładów

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;

Rozpakowywanie w Mono jest procesem rzutowania wskaźnika na przesunięcie 2 gpointerów w obiekcie (np. 16 bajtów). A gpointerjest a void*. Ma to sens, gdy patrzymy na definicję, MonoObjectponieważ jest to po prostu nagłówek danych.

C ++

Aby wstawić wartość w C ++, możesz zrobić coś takiego:

#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;
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.