389 Stimmen

Warum brauchen wir Boxing und Unboxing in C#?

Warum brauchen wir Boxing und Unboxing in C#?

Ich weiß, was Boxen und Unboxing ist, aber ich kann den wirklichen Nutzen nicht begreifen. Warum und wo sollte ich es verwenden?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing

538voto

jason Punkte 227577

Warum

Um ein einheitliches Typsystem zu haben und Wertetypen eine völlig andere Darstellung der ihnen zugrunde liegenden Daten zu ermöglichen als Referenztypen (z. B. ein int ist nur ein Eimer mit zweiunddreißig Bits, was etwas völlig anderes ist als ein Referenztyp).

Stellen Sie sich das folgendermaßen vor. Sie haben eine Variable o vom Typ object . Und jetzt haben Sie eine int und Sie wollen es in o . o ist ein Verweis auf irgendetwas, und die int ist ausdrücklich kein Verweis auf irgendetwas (es ist ja nur eine Zahl). Was Sie also tun, ist Folgendes: Sie erstellen eine neue object die die int und dann weisen Sie einen Verweis auf dieses Objekt in o . Wir nennen diesen Vorgang "Boxen".

Wenn Sie sich also nicht um ein einheitliches Typsystem kümmern (d.h. Referenztypen und Werttypen haben sehr unterschiedliche Darstellungen und Sie wollen keine gemeinsame Art, die beiden zu "repräsentieren"), dann brauchen Sie kein Boxing. Wenn es Ihnen nicht wichtig ist, dass int ihren zugrundeliegenden Wert darstellen (d.h. stattdessen haben int ebenfalls Referenztypen sind und nur einen Verweis auf ihren zugrundeliegenden Wert speichern), dann brauchen Sie kein Boxing.

wo sollte ich es verwenden.

Zum Beispiel, die alte Sammlungsart ArrayList isst nur object s. Das heißt, es werden nur Verweise auf Dinge gespeichert, die irgendwo leben. Ohne Boxing kann man ein int in eine solche Sammlung aufzunehmen. Aber mit Boxen können Sie das.

Heute, in den Tagen der Generika, braucht man das nicht mehr und kann im Allgemeinen fröhlich weitermachen, ohne darüber nachzudenken. Aber es gibt ein paar Vorbehalte, die man beachten sollte:

Das ist richtig:

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

Das ist nicht der Fall:

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

Stattdessen müssen Sie dies tun:

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

Zuerst müssen wir explizit die double ( (double)o ) und diese dann in eine int .

Was ist das Ergebnis des folgenden:

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 zum nächsten Satz übergehen.

Wenn Sie sagen True y False Großartig! Moment, was? Das kommt daher, dass == auf Referenztypen verwendet Referenz-Gleichheit, die prüft, ob die Referenzen gleich sind, nicht ob die zugrunde liegenden Werte gleich sind. Dies ist ein gefährlicher Fehler, den man leicht machen kann. Vielleicht noch subtiler

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

wird auch gedruckt False !

Besser wäre es zu sagen:

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

die dann glücklicherweise Folgendes druckt True .

Eine letzte Spitzfindigkeit:

[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 das Ergebnis? Es kommt darauf an! Wenn Point es un struct dann lautet die Ausgabe 1 aber wenn Point es un class dann lautet die Ausgabe 2 ! Eine Boxing-Konvertierung macht eine Kopie des Wertes, der geboxt wird, was den Unterschied im Verhalten erklärt.

72voto

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

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

In den alten Tagen (1.0 von Microsoft.NET) gab es dieses neumodische generische Tohuwabohu noch nicht. Man konnte keine Methode schreiben, die ein einziges Argument hatte, das einen Werttyp und einen Referenztyp bedienen konnte. Das wäre ein Verstoß gegen die Polymorphie. Also wurde Boxing als Mittel eingeführt, um einen Wertetyp in ein Objekt zu zwingen.

Wäre dies nicht möglich, wäre der Rahmen mit Methoden und Klassen übersät, deren einziger Zweck es ist, die andere Art von Typ zu akzeptieren. Und nicht nur das: Da Wertetypen nicht wirklich einen gemeinsamen Vorfahren haben, müsste man für jeden Wertetyp (Bit, Byte, int16, int32, etc. etc.) eine eigene Methodenüberladung haben.

Der Boxsport hat dies verhindert. Aus diesem Grund feiern die Briten den Boxing Day.

70voto

Chris Moschini Punkte 34996

Der beste Weg, dies zu verstehen, ist ein Blick auf niedrigere Programmiersprachen, auf denen C# aufbaut.

In den niedrigsten Sprachen wie C werden alle Variablen an einem Ort gespeichert: Der Stapel. Jedes Mal, wenn Sie eine Variable deklarieren, kommt sie auf den Stack. Dabei kann es sich nur um primitive Werte handeln, wie bool, byte, 32-bit int, 32-bit uint, usw. Der Stack ist sowohl einfach als auch schnell. Wenn Variablen hinzugefügt werden, kommen sie einfach übereinander, so dass die erste Variable, die Sie deklarieren, z. B. bei 0x00 liegt, die nächste bei 0x01, die nächste bei 0x02 im RAM, usw. Außerdem sind die Variablen oft schon bei der Kompilierung voradressiert, so dass ihre Adresse schon bekannt ist, bevor Sie das Programm starten.

In der nächsthöheren Stufe, wie C++, wird eine zweite Speicherstruktur, der Heap, eingeführt. Sie leben immer noch hauptsächlich im Stack, aber spezielle Ints namens Wegweiser können dem Stack hinzugefügt werden, die die Speicheradresse für das erste Byte eines Objekts speichern, und dieses Objekt befindet sich im Heap. Der Heap ist ein ziemliches Durcheinander und etwas teuer in der Wartung, denn im Gegensatz zu Stack-Variablen stapeln sie sich nicht linear auf und ab, 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 von Speicherlecks, Pufferüberläufen und Frustration. C# als Retter in der Not.

Auf einer höheren Ebene, in C#, müssen Sie sich keine Gedanken über Zeiger machen - das (in C++ geschriebene) .Net-Framework denkt für Sie darüber nach und stellt sie Ihnen als Verweise auf Objekte dar, und aus Leistungsgründen können Sie einfachere Werte wie Bools, Bytes und Ints als Value Types speichern. Unter der Haube werden Objekte und Dinge, die eine Klasse instanziieren, auf dem teuren, speicherverwalteten Heap abgelegt, während Werttypen in demselben Stack gespeichert werden, den Sie auch in low-level C hatten - superschnell.

Um die Interaktion zwischen diesen beiden grundlegend unterschiedlichen Konzepten von Speicher (und Strategien für die Speicherung) aus der Sicht eines Programmierers einfach zu halten, können Wertetypen jederzeit in Boxen gespeichert werden. Boxing bewirkt, dass der Wert vom Stapel kopiert, in ein Objekt eingefügt und auf dem Heap abgelegt wird - teurer, aber flüssige Interaktion mit der Referenzwelt. Wie andere Antworten darauf hinweisen, wird dies auftreten, wenn Sie zum Beispiel sagen:

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

Ein deutliches Beispiel für den Vorteil des Boxens ist die Prü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 Stack, die auf eine Kopie unseres bool b zeigt, das auf den Heap kopiert wurde. Wir können o auf Null prüfen, weil das bool in eine Box gepackt und dort abgelegt wurde.

Im Allgemeinen sollten Sie Boxing vermeiden, es sei denn, Sie brauchen es, zum Beispiel um ein int/bool/whatever als Objekt an ein Argument zu übergeben. Es gibt einige grundlegende Strukturen in .Net, die immer noch die Übergabe von Werttypen als Objekt (und damit Boxing) erfordern, aber in den meisten Fällen sollten Sie nie Boxing benötigen.

Eine nicht erschöpfende Liste historischer C#-Strukturen, die Boxing erfordern und die Sie vermeiden sollten:

  • Das Ereignissystem stellt sich heraus, dass es eine Race Condition gibt in naiver Verwendung, und es unterstützt keine async. Fügen Sie das Boxing-Problem hinzu und es sollte wahrscheinlich vermieden werden. (Sie könnten es zum Beispiel durch ein asynchrones Ereignissystem ersetzen, das Generics verwendet).

  • Die alten Threading- und Timer-Modelle erzwangen eine Box für ihre Parameter, wurden aber durch async/await ersetzt, die weitaus sauberer und effizienter sind.

  • Die .Net 1.1 Collections basierten vollständig auf Boxing, weil sie vor den Generics kamen. Diese sind immer noch in System.Collections zu finden. In jedem neuen Code sollten Sie die Collections von System.Collections.Generic verwenden, die neben der Vermeidung von Boxen auch eine höhere Typensicherheit bieten .

Sie sollten es vermeiden, Ihre Wertetypen als Objekte zu deklarieren oder zu übergeben, es sei denn, Sie müssen sich mit den oben genannten historischen Problemen auseinandersetzen, die das Boxing erzwingen, und Sie wollen den Leistungsverlust durch das spätere Boxing vermeiden, wenn Sie wissen, dass es sowieso geboxt wird.

Wie von Mikael unten vorgeschlagen:

Dies tun

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

Update

In dieser Antwort wurde ursprünglich angenommen, dass Int32, Bool usw. Boxing verursachen, obwohl sie in Wirklichkeit nur Aliase für Werttypen sind. Das heißt, .Net hat Typen wie Bool, Int32, String, und C# Aliase sie zu Bool, Int, String, ohne jede funktionale Unterschied.

27voto

Ray Punkte 21047

Boxing ist nicht wirklich etwas, das Sie verwenden - es ist etwas, das die Laufzeit verwendet, damit Sie Referenz- und Werttypen bei Bedarf auf dieselbe Weise behandeln können. Wenn Sie z. B. eine ArrayList verwenden, um eine Liste von Ganzzahlen zu speichern, werden die Ganzzahlen so gepackt, dass sie in die objektartigen Slots der ArrayList passen.

Bei der Verwendung von generischen Sammlungen ist dies nun nicht mehr der Fall. Wenn Sie eine List<int> wird nicht geboxt - die List<int> kann die ganzen Zahlen direkt enthalten.

15voto

STW Punkte 42452

Boxing und Unboxing werden speziell verwendet, um wertartige Objekte als referenzartige zu behandeln; ihr tatsächlicher Wert wird in den verwalteten Heap verschoben und der Zugriff auf ihren Wert erfolgt über eine Referenz.

Ohne Boxing und Unboxing könnten Sie Wertetypen niemals per Referenz übergeben; und das bedeutet, dass Sie Wertetypen nicht als Instanzen von Object übergeben können.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X