Warum brauchen wir Boxen und Unboxen in C #?

323

Warum brauchen wir Boxen und Unboxen in C #?

Ich weiß, was Boxen und Unboxen ist, aber ich kann den tatsächlichen Gebrauch nicht verstehen. Warum und wo soll ich es verwenden?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing
Vaibhav Jain
quelle

Antworten:

480

Warum

Ein einheitliches Typsystem zu haben und Werttypen eine völlig andere Darstellung ihrer zugrunde liegenden Daten zu ermöglichen als Referenztypen ihre zugrunde liegenden Daten darstellen (z. B. ist ein intnur ein Eimer mit zweiunddreißig Bits, der sich vollständig von einer Referenz unterscheidet Art).

Stellen Sie sich das so vor. Sie haben eine Variable ovom Typ object. Und jetzt hast du eine intund du willst sie hineinstecken o. oist ein Verweis auf etwas irgendwo, und das intist nachdrücklich kein Verweis auf etwas irgendwo (schließlich ist es nur eine Zahl). Sie tun also Folgendes: Sie erstellen ein neues Objekt, in objectdem das gespeichert werden kann, intund weisen diesem Objekt dann einen Verweis zu o. Wir nennen diesen Prozess "Boxen".

Wenn Sie sich also nicht für ein einheitliches Typsystem interessieren (dh Referenztypen und Werttypen haben sehr unterschiedliche Darstellungen und Sie möchten keine gemeinsame Art, die beiden "darzustellen"), brauchen Sie kein Boxen. Wenn es Ihnen nicht wichtig ist, intden zugrunde liegenden Wert darzustellen (dh stattdessen intauch Referenztypen zu sein und nur einen Verweis auf den zugrunde liegenden Wert zu speichern), brauchen Sie kein Boxen.

wo soll ich es benutzen

Zum Beispiel ArrayListisst der alte Sammlungstyp nur objects. Das heißt, es werden nur Verweise auf Dinge gespeichert, die irgendwo leben. Ohne Boxen kann man keine in eine intsolche Sammlung aufnehmen. Aber mit Boxen können Sie.

In den Tagen der Generika braucht man das nicht wirklich und kann im Allgemeinen fröhlich mitmachen, ohne über das Problem nachzudenken. Es sind jedoch einige Einschränkungen zu beachten:

Das ist richtig:

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

Das ist nicht:

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

Stattdessen müssen Sie Folgendes tun:

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

Zuerst müssen wir das double( (double)o) explizit entpacken und dann in ein umwandeln int.

Was ist das Ergebnis von Folgendem:

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

Denken Sie eine Sekunde darüber nach, bevor Sie mit dem nächsten Satz fortfahren.

Wenn du gesagt hast Trueund Falsegroßartig! Warte was? Dies liegt daran, dass ==bei Referenztypen die Referenzgleichheit verwendet wird, die prüft, ob die Referenzen gleich sind, und nicht, ob die zugrunde liegenden Werte gleich sind. Dies ist ein gefährlich leichter Fehler. Vielleicht noch subtiler

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

wird auch gedruckt False!

Besser zu sagen:

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

die dann zum Glück drucken wird True.

Eine letzte Subtilität:

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

Was ist die Ausgabe? Es hängt davon ab, ob! Wenn a Pointist, structdann ist die Ausgabe, 1aber wenn a Pointist, classdann ist die Ausgabe 2! Bei einer Boxkonvertierung wird eine Kopie des zu boxenden Werts erstellt, um den Unterschied im Verhalten zu erläutern.

Jason
quelle
@Jason Wollen Sie damit sagen, dass es keinen Grund gibt, Boxing / Unboxing zu verwenden, wenn wir primitive Listen haben?
Pacerier
Ich bin mir nicht sicher, was du mit "primitiver Liste" meinst.
Jason
3
Könnten Sie bitte mit den Auswirkungen von boxingund auf die Leistung sprechen unboxing?
Kevin Meredith
@ KevinMeredith gibt es eine grundlegende Erklärung zur Leistung für Box- und Unboxing-Operationen in msdn.microsoft.com/en-us/library/ms173196.aspx
InfZero
2
Hervorragende Antwort - besser als die meisten Erklärungen, die ich in angesehenen Büchern gelesen habe.
FredM
58

Im .NET Framework gibt es zwei Arten von Typen - Werttypen und Referenztypen. Dies ist in OO-Sprachen relativ häufig.

Eines der wichtigen Merkmale objektorientierter Sprachen ist die Fähigkeit, Instanzen typunabhängig zu behandeln. Dies wird als Polymorphismus bezeichnet . Da wir den Polymorphismus nutzen wollen, aber zwei verschiedene Arten von Typen haben, muss es eine Möglichkeit geben, sie zusammenzubringen, damit wir mit der einen oder anderen auf die gleiche Weise umgehen können.

In früheren Zeiten (1.0 von Microsoft.NET) gab es dieses neue Hullabaloo für Generika nicht. Sie konnten keine Methode mit einem einzigen Argument schreiben, das einen Werttyp und einen Referenztyp bedienen kann. Das ist eine Verletzung des Polymorphismus. Daher wurde das Boxen als Mittel eingesetzt, um einen Werttyp in ein Objekt zu zwingen.

Wenn dies nicht möglich wäre, würde das Framework mit Methoden und Klassen übersät sein, deren einziger Zweck darin bestand, die anderen Arten des Typs zu akzeptieren. Darüber hinaus müssten Werttypen für jeden Werttyp (Bit, Byte, int16, int32 usw. usw. usw.) eine andere Methodenüberladung aufweisen, da Werttypen nicht wirklich einen gemeinsamen Vorfahren haben.

Das Boxen verhinderte dies. Und deshalb feiern die Briten den Boxing Day.


quelle
1
Vor Generika war Auto-Boxen notwendig, um viele Dinge zu tun; Angesichts der Existenz von Generika wäre es jedoch ohne die implizite Box-Konvertierung besser, wenn .net nicht kompatibel mit altem Code wäre. Gießen einen Werttypen wie List<string>.Enumeratorzum IEnumerator<string>ergeben ein Objekt , das hauptsächlich wie ein Klassentyp verhält, jedoch mit einer aufgebrochen EqualsMethode. Eine bessere Möglichkeit zur Besetzung sein würde , einen benutzerdefinierten Konvertierungsoperator zu rufen, sondern die Existenz einer impliziten Umwandlung verhindert , dass. List<string>.EnumeratorIEnumerator<string>
Supercat
42

Der beste Weg, dies zu verstehen, besteht darin, sich die untergeordneten Programmiersprachen anzusehen, auf denen C # aufbaut.

In den Sprachen der untersten Ebene wie C gehen alle Variablen an eine Stelle: The Stack. Jedes Mal, wenn Sie eine Variable deklarieren, wird sie auf dem Stapel abgelegt. Sie können nur primitive Werte sein, wie ein Bool, ein Byte, ein 32-Bit-Int, ein 32-Bit-Uint usw. Der Stapel ist sowohl einfach als auch schnell. Wenn Variablen hinzugefügt werden, werden sie einfach übereinander gelegt. Der erste, den Sie deklarieren, befindet sich beispielsweise bei 0x00, der nächste bei 0x01, der nächste bei 0x02 im RAM usw. Außerdem werden Variablen beim Kompilieren häufig voradressiert. Zeit, so dass ihre Adresse bekannt ist, bevor Sie das Programm überhaupt ausführen.

In der nächsten Stufe wird wie in C ++ eine zweite Speicherstruktur namens Heap eingeführt. Sie leben immer noch hauptsächlich im Stapel, aber dem Stapel können spezielle Ints hinzugefügt werden, die als Zeiger bezeichnet werden und die Speicheradresse für das erste Byte eines Objekts speichern, und dieses Objekt befindet sich im Heap. Der Heap ist eine Art Chaos und in der Wartung etwas teuer, da sie sich im Gegensatz zu Stack-Variablen nicht linear auf und ab stapeln, wenn ein Programm ausgeführt wird. Sie können in keiner bestimmten Reihenfolge kommen und gehen, und sie können wachsen und schrumpfen.

Der Umgang mit Zeigern ist schwierig. Sie sind die Ursache für Speicherlecks, Pufferüberläufe und Frustration. C # zur Rettung.

Auf einer höheren Ebene, C #, müssen Sie nicht über Zeiger nachdenken - das .Net-Framework (in C ++ geschrieben) berücksichtigt diese für Sie und präsentiert sie Ihnen als Verweise auf Objekte. Aus Gründen der Leistung können Sie einfachere Werte speichern wie Bools, Bytes und Ints als Werttypen. Unter der Haube befinden sich Objekte und Dinge, die eine Klasse instanziieren, auf dem teuren, speicherverwalteten Heap, während Werttypen in demselben Stapel abgelegt werden, den Sie in C auf niedriger Ebene hatten - superschnell.

Um die Interaktion zwischen diesen beiden grundlegend unterschiedlichen Speicherkonzepten (und Speicherstrategien) aus Sicht eines Codierers einfach zu halten, können Werttypen jederzeit eingerahmt werden. Durch das Boxen wird der Wert vom Stapel kopiert, in ein Objekt eingefügt und auf den Haufen gelegt - teurere, aber flüssige Interaktion mit der Referenzwelt. Wie andere Antworten zeigen, geschieht dies, wenn Sie beispielsweise sagen:

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

Ein starkes Beispiel für den Vorteil des Boxens ist die Überprüfung auf Null:

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

Unser Objekt o ist technisch gesehen eine Adresse im Stapel, die auf eine Kopie unseres Bools b verweist, die auf den Heap kopiert wurde. Wir können o auf null prüfen, weil der Bool verpackt und dort abgelegt wurde.

Im Allgemeinen sollten Sie Boxen vermeiden, es sei denn, Sie benötigen es, um beispielsweise ein int / bool / was auch immer als Objekt an ein Argument zu übergeben. Es gibt einige grundlegende Strukturen in .Net, die weiterhin die Übergabe von Werttypen als Objekt erfordern (und daher Boxing erfordern), aber zum größten Teil sollten Sie niemals Boxen benötigen.

Eine nicht erschöpfende Liste historischer C # -Strukturen, für die Boxen erforderlich ist, die Sie vermeiden sollten:

  • Es stellt sich heraus, dass das Ereignissystem eine Race-Bedingung hat, die naiv verwendet wird, und es unterstützt keine Asynchronisierung. Fügen Sie das Boxproblem hinzu und es sollte wahrscheinlich vermieden werden. (Sie können es beispielsweise durch ein asynchrones Ereignissystem ersetzen, das Generics verwendet.)

  • Die alten Threading- und Timer-Modelle haben eine Box auf ihre Parameter gezwungen, wurden jedoch durch Async / Warten ersetzt, die weitaus sauberer und effizienter sind.

  • Die .Net 1.1-Sammlungen stützten sich vollständig auf das Boxen, da sie vor Generics kamen. Diese treten in System.Collections immer noch auf. In jedem neuen Code sollten Sie die Sammlungen von System.Collections.Generic verwenden, die Ihnen neben der Vermeidung des Boxens auch eine stärkere Typensicherheit bieten .

Sie sollten vermeiden, Ihre Werttypen als Objekte zu deklarieren oder zu übergeben, es sei denn, Sie müssen sich mit den oben genannten historischen Problemen befassen, die das Boxen erzwingen, und Sie möchten den Leistungseinbruch des späteren Boxens vermeiden, wenn Sie wissen, dass es sowieso boxen wird.

Per Mikaels Vorschlag unten:

Mach das

using System.Collections.Generic;

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

Nicht das

using System.Collections;

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

Aktualisieren

Diese Antwort schlug ursprünglich vor, dass Int32, Bool usw. Boxen verursachen, obwohl es sich tatsächlich um einfache Aliase für Werttypen handelt. Das heißt, .Net verfügt über Typen wie Bool, Int32, String und C #, die ohne funktionalen Unterschied in Bool, Int, String umgewandelt werden.

Chris Moschini
quelle
4
Sie haben mir beigebracht, was hundert Programmierer und IT-Profis in Jahren nicht erklären konnten, aber ändern Sie es, um zu sagen, was Sie tun sollten , anstatt zu vermeiden, weil es ein bisschen schwierig wurde, Grundregeln zu befolgen Sie sollen dies nicht tun, sondern dies tun
Mikael Puusaari
2
Diese Antwort hätte hundertmal als ANTWORT markiert werden müssen!
Pouyan
3
Es gibt kein "Int" in c #, es gibt int und Int32. Ich glaube, Sie irren sich, wenn Sie angeben, dass einer ein Werttyp und der andere ein Referenztyp ist, der den Werttyp umschließt. Wenn ich mich nicht irre, ist das in Java wahr, aber nicht in C #. In C # sind diejenigen, die in der IDE blau angezeigt werden, Aliase für ihre Strukturdefinition. Also: int = Int32, bool = Boolean, string = String. Der Grund für die Verwendung von Bool über Boolean liegt darin, dass dies in den MSDN-Entwurfsrichtlinien und -Konventionen empfohlen wird. Ansonsten liebe ich diese Antwort. Aber ich werde abstimmen, bis Sie mir das Gegenteil beweisen oder dies in Ihrer Antwort korrigieren.
Heriberto Lugo
2
Wenn Sie eine Variable als int und eine andere als Int32 oder bool und Boolean deklarieren - Rechtsklick und Ansichtsdefinition, erhalten Sie dieselbe Definition für eine Struktur.
Heriberto Lugo
2
@HeribertoLugo ist korrekt, die Zeile "Sie sollten vermeiden, Ihre Werttypen als Bool anstelle von Bool zu deklarieren" ist falsch. Wie OP hervorhebt, sollten Sie vermeiden, Ihren Bool (oder Boolean oder einen anderen Werttyp) als Objekt zu deklarieren. bool / Boolean, int / Int32, sind nur Aliase zwischen C # und .NET: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
STW
21

Boxen ist nicht wirklich etwas, das Sie verwenden - es ist etwas, das die Laufzeit verwendet, damit Sie Referenz- und Werttypen bei Bedarf auf die gleiche Weise behandeln können. Wenn Sie beispielsweise eine ArrayList zum Speichern einer Liste von Ganzzahlen verwendet haben, wurden die Ganzzahlen so eingerahmt, dass sie in die Objekttyp-Slots in der ArrayList passen.

Wenn Sie jetzt generische Sammlungen verwenden, verschwindet dies so ziemlich. Wenn Sie ein erstellen List<int>, wird kein Boxen durchgeführt - das List<int>kann die ganzen Zahlen direkt enthalten.

Strahl
quelle
Sie müssen noch boxen, um beispielsweise zusammengesetzte Zeichenfolgen zu formatieren. Sie werden es vielleicht nicht so oft sehen, wenn Sie Generika verwenden, aber es ist definitiv immer noch da.
Jeremy S
1
true - es wird auch in ADO.NET ständig angezeigt - SQL-Parameterwerte sind alle 'Objekte, unabhängig vom tatsächlichen Datentyp
Ray
11

Boxing und Unboxing werden speziell verwendet, um Objekte vom Werttyp als Referenztyp zu behandeln. Verschieben ihres tatsächlichen Werts auf den verwalteten Heap und Zugreifen auf ihren Wert als Referenz.

Ohne Boxing und Unboxing könnten Sie Werttypen niemals als Referenz übergeben. und das bedeutet, dass Sie keine Werttypen als Instanzen von Object übergeben konnten.

STW
quelle
immer noch schöne Antwort nach fast 10 Jahren Sir +1
snr
1
Die Übergabe von numerischen Typen als Referenz ist in Sprachen ohne Boxing vorhanden, und andere Sprachen implementieren die Behandlung von Werttypen als Instanzen von Object ohne Boxing und das Verschieben des Werts auf den Heap (z. B. Implementierungen dynamischer Sprachen, bei denen Zeiger an 4-Byte-Grenzen ausgerichtet sind, verwenden die unteren vier Referenzbits, die angeben, dass der Wert eher eine Ganzzahl oder ein Symbol als ein vollständiges Objekt ist (solche Werttypen sind unveränderlich und haben dieselbe Größe wie ein Zeiger).
Pete Kirkham
8

Der letzte Ort, an dem ich etwas entpacken musste, war das Schreiben von Code, der einige Daten aus einer Datenbank abrief (ich habe LINQ to SQL nicht verwendet, sondern nur altes ADO.NET ):

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

Wenn Sie vor Generika mit älteren APIs arbeiten, werden Sie grundsätzlich auf Boxen stoßen. Davon abgesehen ist es nicht so üblich.

BKostenlos
quelle
4

Boxing ist erforderlich, wenn wir eine Funktion haben, die ein Objekt als Parameter benötigt, aber verschiedene Werttypen übergeben werden müssen. In diesem Fall müssen wir zuerst Werttypen in Objektdatentypen konvertieren, bevor wir sie an die Funktion übergeben.

Ich glaube nicht, dass das stimmt. Versuchen Sie stattdessen Folgendes:

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

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

Das läuft gut, ich habe kein Boxen / Unboxen benutzt. (Es sei denn, der Compiler macht das hinter den Kulissen?)

Manoj
quelle
Das liegt daran, dass alles von System.Object erbt und Sie der Methode ein Objekt mit zusätzlichen Informationen geben. Im Grunde rufen Sie die Testmethode mit dem auf, was sie erwartet und was sie erwartet, da sie nichts Besonderes erwartet. Hinter den Kulissen wird viel in .NET getan, und der Grund, warum es eine sehr einfache Sprache ist
Mikael Puusaari
1

In .net enthält jede Instanz von Object oder ein davon abgeleiteter Typ eine Datenstruktur, die Informationen zu ihrem Typ enthält. "Echte" Werttypen in .net enthalten keine solchen Informationen. Damit Daten in Werttypen von Routinen bearbeitet werden können, die vom Objekt abgeleitete Typen empfangen, definiert das System automatisch für jeden Werttyp einen entsprechenden Klassentyp mit denselben Elementen und Feldern. Durch das Boxen werden neue Instanzen dieses Klassentyps erstellt, wobei die Felder von einer Werttypinstanz kopiert werden. Beim Unboxing werden die Felder von einer Instanz des Klassentyps in eine Instanz des Werttyps kopiert. Alle Klassentypen, die aus Werttypen erstellt werden, werden von der ironisch benannten Klasse ValueType abgeleitet (die trotz ihres Namens tatsächlich ein Referenztyp ist).

Superkatze
quelle
0

Wenn eine Methode nur einen Referenztyp als Parameter verwendet (z. B. eine generische Methode, die über die Einschränkung auf eine Klasse newbeschränkt ist), können Sie keinen Referenztyp an sie übergeben und müssen ihn einschließen.

Dies gilt auch für alle Verfahren wahr , die nehmen objectals Parameter - dies hat ein Referenztyp sein.

Oded
quelle
0

Im Allgemeinen sollten Sie das Boxen Ihrer Werttypen vermeiden.

Es gibt jedoch seltene Fälle, in denen dies nützlich ist. Wenn Sie beispielsweise auf das 1.1-Framework abzielen müssen, haben Sie keinen Zugriff auf die generischen Sammlungen. Für jede Verwendung der Sammlungen in .NET 1.1 muss Ihr Werttyp als System.Object behandelt werden, was zu Boxing / Unboxing führt.

Es gibt immer noch Fälle, in denen dies in .NET 2.0+ nützlich ist. Jedes Mal, wenn Sie die Tatsache nutzen möchten, dass alle Typen, einschließlich Werttypen, direkt als Objekt behandelt werden können, müssen Sie möglicherweise Boxing / Unboxing verwenden. Dies kann manchmal nützlich sein, da Sie damit jeden Typ in einer Sammlung speichern können (indem Sie Objekt anstelle von T in einer generischen Sammlung verwenden). Im Allgemeinen ist es jedoch besser, dies zu vermeiden, da Sie die Typensicherheit verlieren. Der einzige Fall, in dem Boxen häufig auftritt, ist die Verwendung von Reflection. Bei vielen Aufrufen in Reflection ist Boxing / Unboxing erforderlich, wenn mit Werttypen gearbeitet wird, da der Typ nicht im Voraus bekannt ist.

Hunain
quelle
0

Boxing ist die Konvertierung eines Werts in einen Referenztyp, bei dem die Daten in einem Objekt auf dem Heap versetzt sind.

Was das Boxen eigentlich macht. Hier sind einige Beispiele

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;

Beim Unboxing in Mono wird ein Zeiger mit einem Versatz von 2 gpointern im Objekt (z. B. 16 Byte) umgewandelt. A gpointerist a void*. Dies ist sinnvoll, wenn man sich die Definition von betrachtet, MonoObjectda es sich eindeutig nur um eine Kopfzeile für die Daten handelt.

C ++

Um einen Wert in C ++ zu boxen, können Sie Folgendes tun:

#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;
}
Lewis Kelsey
quelle