Perché abbiamo bisogno di boxe e unboxing in C #?


325

Perché abbiamo bisogno di boxe e unboxing in C #?

So cos'è la boxe e unboxing, ma non riesco a comprenderne il reale utilizzo. Perché e dove dovrei usarlo?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing

Risposte:


482

Perché

Avere un sistema di tipi unificato e consentire ai tipi di valore di avere una rappresentazione completamente diversa dei loro dati sottostanti dal modo in cui i tipi di riferimento rappresentano i loro dati sottostanti (ad es. int è solo un bucket di trentadue bit che è completamente diverso da un riferimento genere).

Pensala in questo modo. Hai una variabile odi tipo object. E ora ne hai uno inte vuoi inserirlo o. oè un riferimento a qualcosa da qualche parte, e intnon è decisamente un riferimento a qualcosa da qualche parte (dopo tutto, è solo un numero). Quindi, quello che fai è questo: fai un nuovo objectche può memorizzare il inte quindi assegni un riferimento a quell'oggetto o. Questo processo viene chiamato "boxe".

Quindi, se non ti interessa avere un sistema di tipi unificato (ad esempio, i tipi di riferimento e i tipi di valore hanno rappresentazioni molto diverse e non vuoi un modo comune di "rappresentare" i due) allora non hai bisogno di boxe. Se non ti importa di aver intrappresentato il loro valore sottostante (ad esempio, invece hanno intanche tipi di riferimento e memorizza solo un riferimento al loro valore sottostante), non hai bisogno di boxe.

dove dovrei usarlo.

Ad esempio, il vecchio tipo di raccolta ArrayListmangia solo objects. Cioè, memorizza solo riferimenti a qualcosa che vive da qualche parte. Senza boxe non è possibile inserire un inttale in una raccolta. Ma con la boxe puoi farlo.

Ora, ai tempi dei generici non hai davvero bisogno di questo e generalmente puoi andare allegramente senza pensare al problema. Ma ci sono alcuni avvertimenti da tenere presente:

Questo è corretto:

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

Questo non è:

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

Invece devi farlo:

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

Per prima cosa dobbiamo decomprimere esplicitamente il double( (double)o) e poi lanciarlo in unint .

Qual è il risultato di quanto segue:

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

Pensaci un secondo prima di passare alla frase successiva.

Se hai detto Truee Falsefantastico! Aspetta cosa? Questo perché ==sui tipi di riferimento utilizza l'uguaglianza di riferimento che verifica se i riferimenti sono uguali, non se i valori sottostanti sono uguali. Questo è un errore pericolosamente facile da fare. Forse ancora più sottile

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

stamperà anche False !

Meglio dire:

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

che poi, per fortuna, stamperà True .

Un'ultima sottigliezza:

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

Qual è l'output? Dipende! Se Pointè un structallora l'output è 1ma se Pointè un classallora l'output è 2! Una conversione di boxe crea una copia del valore in box spiegando la differenza di comportamento.


@Jason Intendi dire che se abbiamo elenchi primitivi, non c'è motivo di usare qualsiasi boxe / unboxing?
Pacerier

Non sono sicuro di cosa intendi per "elenco primitivo".
Jason

3
Potresti per favore parlare dell'impatto sulle prestazioni di boxinge unboxing?
Kevin Meredith,

@KevinMeredith v'è una spiegazione di base sulle prestazioni per la boxe e le operazioni unboxing in msdn.microsoft.com/en-us/library/ms173196.aspx
InfZero

2
Risposta eccellente - meglio della maggior parte delle spiegazioni che ho letto in libri ben considerati.
FredM,

59

Nel framework .NET, ci sono due specie di tipi: tipi di valore e tipi di riferimento. Questo è relativamente comune nelle lingue OO.

Una delle caratteristiche importanti dei linguaggi orientati agli oggetti è la capacità di gestire le istanze in modo indipendente dal tipo. Questo è indicato come polimorfismo . Dato che vogliamo sfruttare il polimorfismo, ma abbiamo due diverse specie di tipi, ci deve essere un modo per riunirli in modo da poter gestire l'uno o l'altro allo stesso modo.

Ora, ai vecchi tempi (1.0 di Microsoft.NET), non esisteva questo hullabaloo generici di nuova generazione. Non è stato possibile scrivere un metodo con un singolo argomento in grado di servire un tipo di valore e un tipo di riferimento. Questa è una violazione del polimorfismo. Quindi il pugilato è stato adottato come mezzo per forzare un tipo di valore in un oggetto.

Se ciò non fosse possibile, la struttura sarebbe disseminata di metodi e classi il cui unico scopo era quello di accettare le altre specie di tipo. Non solo, ma poiché i tipi di valore non condividono realmente un antenato di tipo comune, dovresti avere un sovraccarico del metodo diverso per ciascun tipo di valore (bit, byte, int16, int32, ecc ecc ecc).

La boxe ha impedito che ciò accadesse. Ed è per questo che gli inglesi celebrano il Santo Stefano.


1
Prima dei generici, l'auto-boxing era necessario per fare molte cose; data l'esistenza dei generici, tuttavia, se non fosse per la necessità di mantenere la compatibilità con il vecchio codice, penso che .net sarebbe meglio senza implicite conversioni di boxe. Lanciare un tipo di valore come List<string>.Enumeratorper IEnumerator<string>produrre un oggetto che si comporta principalmente come un tipo di classe, ma con un Equalsmetodo rotto . Un modo migliore per getto List<string>.Enumeratora IEnumerator<string>sarebbe quello di chiamare un operatore di conversione personalizzato, ma l'esistenza di un implicite impedisce conversione.
supercat,

42

Il modo migliore per capirlo è guardare ai linguaggi di programmazione di livello inferiore su cui C # si basa.

Nei linguaggi di livello più basso come C, tutte le variabili vanno in un posto: lo Stack. Ogni volta che dichiari una variabile, questa va in pila. Possono essere solo valori primitivi, come un valore booleano, un byte, un int a 32 bit, un uint a 32 bit, ecc. Lo Stack è sia semplice che veloce. Man mano che le variabili vengono aggiunte, vanno semplicemente una sopra l'altra, quindi la prima che dichiari si trova a dire, 0x00, la successiva a 0x01, la successiva a 0x02 in RAM, ecc. Inoltre, le variabili sono spesso pre-indirizzate durante la compilazione- ora, quindi il loro indirizzo è noto prima ancora di eseguire il programma.

Al livello successivo, come C ++, viene introdotta una seconda struttura di memoria chiamata Heap. Vivi ancora per lo più nello Stack, ma ints speciali chiamati Puntatori possono essere aggiunti allo Stack, che memorizza l'indirizzo di memoria per il primo byte di un Oggetto e quell'oggetto vive nell'Heap. L'heap è un po 'un casino e un po' costoso da mantenere, perché a differenza delle variabili Stack non si accumulano linearmente su e poi giù mentre un programma viene eseguito. Possono andare e venire in nessuna sequenza particolare e possono crescere e restringersi.

Gestire i puntatori è difficile. Sono la causa di perdite di memoria, sovraccarichi del buffer e frustrazione. C # in soccorso.

A un livello superiore, C #, non è necessario pensare ai puntatori: il framework .Net (scritto in C ++) li considera per te e li presenta come riferimenti agli oggetti e, per prestazioni, ti consente di memorizzare valori più semplici come bool, byte e ints come tipi di valore. Sotto il cofano, Oggetti e cose che istanziano una Classe vanno nel costoso Heap gestito dalla memoria, mentre i Tipi di valore vanno nello stesso Stack che avevi in ​​C di basso livello - super veloce.

Al fine di mantenere semplice l'interazione tra questi 2 concetti fondamentalmente diversi di memoria (e strategie di archiviazione) dal punto di vista di un programmatore, i Tipi di valore possono essere inscatolati in qualsiasi momento. La boxe fa sì che il valore venga copiato dallo Stack, messo in un Oggetto e posizionato nell'Heap - interazione più costosa, ma fluida con il mondo di riferimento. Come sottolineato da altre risposte, ciò si verificherà quando ad esempio dici:

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

Un'illustrazione forte del vantaggio del pugilato è un controllo per null:

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

Il nostro oggetto o è tecnicamente un indirizzo nello Stack che punta a una copia del nostro bool b, che è stato copiato nell'Heap. Possiamo controllare o per null perché il bool è stato inscatolato e messo lì.

In generale, dovresti evitare la boxe a meno che tu non ne abbia bisogno, ad esempio per passare un int / bool / qualunque cosa come oggetto a un argomento. Ci sono alcune strutture di base in .Net che richiedono ancora il passaggio di Tipi di valore come oggetto (e quindi richiedono Boxing), ma per la maggior parte non dovresti mai aver bisogno di Box.

Un elenco non esaustivo di strutture storiche C # che richiedono il pugilato, che dovresti evitare:

  • Il sistema di eventi risulta avere una Race Race in uso ingenuo e non supporta l'asincronizzazione. Aggiungi il problema di inscatolamento e probabilmente dovrebbe essere evitato. (È possibile sostituirlo, ad esempio, con un sistema di eventi asincrono che utilizza Generics.)

  • I vecchi modelli di Threading e Timer hanno forzato un Box sui loro parametri ma sono stati sostituiti da asincrono / wait che sono molto più puliti ed efficienti.

  • Le collezioni .Net 1.1 si basavano interamente sulla boxe, perché venivano prima di Generics. Questi stanno ancora andando in giro in System.Collections. In ogni nuovo codice dovresti usare le Collezioni di System.Collections.Generic, che oltre a evitare la boxe ti offrono anche una maggiore sicurezza del tipo .

Dovresti evitare di dichiarare o trasmettere i tuoi Tipi di valore come oggetti, a meno che tu non debba affrontare i problemi storici di cui sopra che forzano il Boxing e vuoi evitare il colpo di prestazione del Boxing in un secondo momento quando sai che verrà comunque inscatolato.

Di seguito il suggerimento di Mikael:

Fai questo

using System.Collections.Generic;

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

Non questo

using System.Collections;

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

Aggiornare

Questa risposta originariamente suggeriva che Int32, Bool ecc. Causassero il pugilato, quando in realtà sono semplici alias per Tipi di valore. Cioè .Net ha tipi come Bool, Int32, String e C # che li alias per bool, int, string, senza alcuna differenza funzionale.


4
Mi hai insegnato ciò che centinaia di programmatori e professionisti IT non potevano spiegare da anni, ma cambiarlo per dire cosa dovresti fare invece di cosa evitare, perché è diventato un po 'difficile da seguire .. le regole di base il più delle volte non vanno 1 non devi fare questo, invece fai questo
Mikael Puusaari,

2
Questa risposta avrebbe dovuto essere contrassegnata come RISPOSTA centinaia di volte!
Pouyan,

3
non c'è "Int" in c #, c'è int e Int32. credo che ti sbagli nell'affermare che uno è un tipo di valore e l'altro è un tipo di riferimento che avvolge il tipo di valore. a meno che non mi sbagli, è vero in Java, ma non in C #. In C # quelli che appaiono in blu nell'IDE sono alias per la loro definizione di struttura. Quindi: int = Int32, bool = Boolean, string = String. Il motivo per utilizzare boolean su Boolean è perché è suggerito nelle linee guida e convenzioni di progettazione MSDN. Altrimenti adoro questa risposta. Ma voterò in giù fino a quando non mi dimostrerai che mi sbaglio o lo risolverò nella tua risposta.
Heriberto Lugo,

2
Se si dichiara una variabile come int e un'altra come Int32, o bool e booleana - fare clic con il tasto destro e visualizzare la definizione, si finirà nella stessa definizione per una struttura.
Heriberto Lugo,

2
@HeribertoLugo è corretto, la riga "Dovresti evitare di dichiarare i tuoi tipi di valore come Bool anziché bool" è errata. Come sottolinea OP, dovresti evitare di dichiarare il tuo valore booleano (o booleano o qualsiasi altro tipo di valore) come oggetto. bool / Boolean, int / Int32, sono solo alias tra C # e .NET: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
STW

21

La boxe non è davvero qualcosa che usi: è qualcosa che il runtime utilizza in modo da poter gestire i tipi di riferimento e valore nello stesso modo quando necessario. Ad esempio, se hai utilizzato un ArrayList per contenere un elenco di numeri interi, i numeri interi sono stati inscatolati per adattarsi agli slot del tipo di oggetto in ArrayList.

Usando raccolte generiche ora, praticamente scompare. Se si crea un List<int>, non viene eseguito alcun boxing: List<int>può contenere direttamente i numeri interi.


Hai ancora bisogno di boxe per cose come la formattazione di stringhe composite. Potresti non vederlo così spesso quando usi i generici, ma è sicuramente ancora lì.
Jeremy S,

1
true - viene visualizzato sempre anche in ADO.NET - i valori dei parametri sql sono tutti 'oggetto, indipendentemente dal tipo di dati reale
Ray

11

Boxing e Unboxing sono specificamente utilizzati per trattare gli oggetti di tipo valore come tipo di riferimento; spostando il loro valore effettivo nell'heap gestito e accedendo al loro valore come riferimento.

Senza boxe e unboxing non potresti mai passare tipi di valore per riferimento; e ciò significa che non è possibile passare tipi di valore come istanze di Object.


ancora bella risposta dopo quasi 10 anni signore +1
snr

1
Passa per riferimento di tipi numerici esiste nelle lingue senza boxe e altre lingue implementano il trattamento dei tipi di valore come istanze di Object senza boxe e spostano il valore nell'heap (ad es. Implementazioni di linguaggi dinamici in cui i puntatori sono allineati a limiti di 4 byte utilizzano i quattro inferiori bit di riferimenti per indicare che il valore è un numero intero o un simbolo anziché un oggetto intero; tali tipi di valore sono immutabili e hanno le stesse dimensioni di un puntatore).
Pete Kirkham,

8

L'ultimo posto in cui ho dovuto decomprimere qualcosa è stato quando ho scritto del codice che ha recuperato alcuni dati da un database (non stavo usando LINQ to SQL , solo un vecchio ADO.NET ):

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

Fondamentalmente, se stai lavorando con API precedenti prima dei farmaci generici, incontrerai il pugilato. A parte questo, non è così comune.


4

La boxe è richiesta, quando abbiamo una funzione che ha bisogno dell'oggetto come parametro, ma abbiamo diversi tipi di valore che devono essere passati, in tal caso dobbiamo prima convertire i tipi di valore in tipi di dati oggetto prima di passarli alla funzione.

Non penso sia vero, prova invece questo:

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

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

Funziona bene, non ho usato la boxe / unboxing. (A meno che il compilatore non lo faccia dietro le quinte?)


Questo perché tutto eredita da System.Object e stai fornendo al metodo un oggetto con informazioni aggiuntive, quindi sostanzialmente stai chiamando il metodo di test con ciò che si aspetta e tutto ciò che potrebbe aspettarsi dal momento che non si aspetta nulla in particolare. Molto in .NET viene fatto dietro le quinte, e il motivo per cui è un linguaggio molto semplice da usare
Mikael Puusaari,

1

In .net, ogni istanza di Object, o qualsiasi tipo derivato da esso, include una struttura di dati che contiene informazioni sul suo tipo. I tipi di valore "reali" in .net non contengono tali informazioni. Per consentire ai dati nei tipi di valore di essere manipolati da routine che prevedono di ricevere tipi derivati ​​dall'oggetto, il sistema definisce automaticamente per ogni tipo di valore un tipo di classe corrispondente con gli stessi membri e campi. La boxe crea nuove istanze di questo tipo di classe, copiando i campi da un'istanza di tipo valore. Unboxing copia i campi da un'istanza del tipo di classe a un'istanza del tipo di valore. Tutti i tipi di classe creati da tipi di valore derivano dalla classe ironicamente denominata ValueType (che, nonostante il nome, è in realtà un tipo di riferimento).


0

Quando un metodo accetta solo un tipo di riferimento come parametro (ad esempio un metodo generico vincolato a essere una classe tramite il newvincolo), non sarà possibile passargli un tipo di riferimento e inserirlo in un box.

Questo vale anche per tutti i metodi che prendono objectcome parametro - questo deve essere un tipo di riferimento.


0

In generale, in genere si desidera evitare l'inscatolamento dei tipi di valore.

Tuttavia, ci sono eventi rari in cui ciò è utile. Se è necessario targetizzare il framework 1.1, ad esempio, non si avrà accesso alle raccolte generiche. Qualsiasi utilizzo delle raccolte in .NET 1.1 richiederebbe il trattamento del tipo di valore come System.Object, che provoca boxe / unboxing.

Ci sono ancora casi in cui ciò può essere utile in .NET 2.0+. Ogni volta che si desidera sfruttare il fatto che tutti i tipi, compresi i tipi di valore, possono essere trattati direttamente come un oggetto, potrebbe essere necessario utilizzare il boxing / unboxing. Questo può essere utile a volte, poiché ti consente di salvare qualsiasi tipo in una raccolta (utilizzando l'oggetto anziché T in una raccolta generica), ma in generale è meglio evitarlo, poiché stai perdendo la sicurezza del tipo. L'unico caso in cui si verifica frequentemente la boxe è quando si utilizza Reflection: molte delle chiamate in reflection richiedono la boxe / unboxing quando si lavora con tipi di valore, poiché il tipo non è noto in anticipo.


0

La boxe è la conversione di un valore in un tipo di riferimento con i dati con un certo offset in un oggetto sull'heap.

Per quanto riguarda ciò che effettivamente fa la boxe. Ecco alcuni esempi

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;

Unboxing in Mono è un processo di lancio di un puntatore con un offset di 2 punti di riferimento nell'oggetto (ad es. 16 byte). A gpointerè a void*. Questo ha senso quando si guarda la definizione di MonoObjectcome è chiaramente solo un'intestazione per i dati.

C ++

Per inserire un valore in C ++ puoi fare qualcosa del tipo:

#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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.