(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

Eine kleine Typenlehre

Veröffentlicht: 15. Dez 2001 | Aktualisiert: 14. Jun 2004

Von Jeffrey Richter

In dieser Ausgabe geht es um die Grundtypen in .NET, um den Unterschied zwischen Werte- und Referenztypen und um das Konvertieren von Werte- in Referenztypen, das sogenannte Boxing.

Auf dieser Seite

Grundtypen
Referenztypen und Werttypen
Boxing und Unboxing
Fazit

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild01



In der letzten Ausgabe habe ich schon einige der grundlegenden Konzepte der Typen aus der Laufzeitschicht von Microsoft .NET vorgestellt. Insbesondere habe ich davon gesprochen, dass sich alle Typen von System.Object ableiten, und einige Mechanismen vorgestellt (zum Beispiel C#-Operatoren), mit denen sich die Typen ineinander umwandeln lassen. Zum Schluss habe ich noch erwähnt, wie die Namensräume von den Compilern benutzt werden und wie sie von der allen Sprachen gemeinsamen Laufzeitschicht von .NET implementiert werden.

In dieser Folge möchte ich nun die Besprechung des Typsystems fortsetzen. Ich beginne mit der Einführung der Grundtypen und fahre dann mit den Referenz- und den Werttypen fort. Für einen Entwickler ist es sehr wichtig, sich mit den Verhaltensweisen der letzteren beiden Typen auszukennen. So bin ich zum Beispiel davon überzeugt, dass ein Entwickler, der sich der Unterschiede zwischen Referenz- und Werttypen nicht bewusst ist, subtile Bugs in seinen Code einbaut und auch die Leistung des Systems nicht optimal ausnutzt.

Grundtypen

Bestimmte Datentypen werden so häufig benutzt, dass manche Compiler für sie eine vereinfachte Syntax zulassen. Eine ganze Zahl können Sie in C# zum Beispiel mit der folgenden Syntax anlegen:

int a = new int(5);

Sicher werden Sie mir zustimmen, dass die Deklaration und Initialisierung einer Integer mit dieser Syntax ziemlich lästig ist. Zum Glück lassen viele Compiler (auch für C#) eine einfachere Syntax wie die folgende zu:

int a = 5;

Dadurch wird der Code übersichtlicher und lesbarer. Die daraus generierte Zwischensprache (intermediate language, IL) ist bei beiden Schreibweisen natürlich identisch.

Die Datentypen, die der Compiler schon von Haus aus anbietet, werden Grundtypen genannt (engl. Primitives). Die Grundtypen lassen sich direkt auf Typen abbilden, die es in der Basisklassenbibliothek gibt. In C# passt zum Beispiel int direkt auf den Typ System.Int32. Deswegen sind die beiden folgenden Zeilen letztlich auch mit den bereits gezeigten Zeilen identisch:

System.Int32 a = new System.Int32(5); 
System.Int32 a = 5;


Tabelle T1 führt die Typen aus der Basisklassenbibliothek auf, für die es in C# entsprechende Grundtypen gibt (andere Sprachen werden ähnliche Grundtypen anbieten).

T1 Grundtypen und ihre Entsprechungen in der Basisklassenbibliothek

C#-Grundtyp

BCL-Typ

Beschreibung

sbyte

System.SByte

8-Bit-Wert mit Vorzeichen

byte

System.Byte

8-Bit-Wert ohne Vorzeichen

short

System.Int16

16-Bit-Wert mit Vorzeichen

ushort

System.UInt16

16-Bit-Wert ohne Vorzeichen

int

System.Int32

32-Bit-Wert mit Vorzeichen

uint

System.UInt32

32-Bit-Wert ohne Vorzeichen

long

System.Int64

64-Bit-Wert mit Vorzeichen

ulong

System.UInt64

64-Bit-Wert ohne Vorzeichen

char

System.Char

16-Bit Unicode-Zeichen

float

System.Single

IEEE-Gleitkommazahl, 32 Bit

double

System.Double

IEEE-Gleitkommazahl, 64 Bit

bool

System.Boolean

Boolscher Wert (wahr, falsch)

decimal

System.Decimal

Eine ganze 96-Bit-Zahl mit Vorzeichen mal 10 exp 0 bis 10 exp 28 (bei Finanzberechnungen üblich, bei denen keine Rundungsfehler geduldet werden)

object

System.Object

Basis aller Typen

string

System.String

Stringtyp

Referenztypen und Werttypen

Wenn ein Objekt auf der "verwalteten Halde" (Managed Heap) angelegt wird, muss der new-Operator die Adresse liefern, die das neue Objekt im Speicher hat. Im Normalfall speichert man diese Adresse in einer Variablen ab. Bei dieser Variablen handelt es sich um einen Referenztypen, weil die Variable nicht selbst die Bits aus dem Objekt enthält, sondern sich nur auf die Objektbits bezieht.

Es gibt einige Dinge, die beim Umgang mit Referenztypen beachtet werden wollen, um sich nicht negativ auf die Leistung auszuwirken. Erstens muss der Speicher von der verwalteten Halde angefordert werden. Folglich könnte die Speicherbeschaffung eine Garbage Collection auslösen. Zweitens werden die referenzierten Typen unter der Haube immer über ihre Zeiger angesprochen. Also muss für jeden Zugriff auf ein Objekt, das auf der Halde liegt, der entsprechende Code für den Zugriff via Zeiger generiert werden, der die gewünschte Aktion ausführt. Und das hat seine Auswirkungen auf Größe und Geschwindigkeit.

Neben den Referenztypen bietet das virtuelle Objektsystem auch noch leichtgewichtigere Typen an, die Werttypen (value types) genannt werden. Werttypobjekte können nicht auf der vom Garbage Collector überwachten Speicherhalde angelegt werden und eine Variable, die solch ein Objekt repräsentiert, enthält keinen Zeiger auf das Objekt, sondern das Objekt höchstselbst. Da schon die Variable das Objekt enthält, braucht für den Zugriff auf das Objekt kein Zeiger dereferenziert zu werden. Das erhöht natürlich die Geschwindigkeit.

L1 Referenztypen und Werttypen

// Referenztyp (wegen des 'class') 
class  RectRef { public int x, y, cx, cy; } 
// Werttyp (wegen des 'struct') 
struct RectVal { public int x, y, cx, cy; } 
static void SomeMethod { 
   RectRef rr1 = new RectRef();  // Auf der Speicherhalde angelegt 
   RectVal rv1;                  // Auf dem Stapel angelegt  
                                 //  (new ist optional) 
   rr1.x = 10;                   // Zeigerdereferenzierung 
   rv1.x = 10;                   // Änderung auf dem Stapel 
   RectRef rr2 = rr1;            // kopiert nur einen Zeiger 
   RectVal rv2 = rv1;            // Anlage auf Stapel,  
                                 //   kopiert Datenelemente 
   rr1.x = 20;                   // Ändert rr1 und rr2 
   rv1.x = 20;                   // Ändert rv1, nicht rv2 
}

Der Code aus Listing L1 zeigt, worin sich die Referenztypen und die Werttypen unterscheiden. Der Rechtecktyp aus Listing L1 wird mit struct deklariert statt mit dem gebräuchlicheren class. In C# ist ein Typ, der mit struct deklariert wird, ein Werttyp, während es sich bei Typen, die mit class deklariert werden, um Referenztypen handelt. In anderen Sprachen dürfte wohl auch eine andere Syntax zur Beschreibung von Werttypen und Referenztypen gebräuchlich sein. So gibt es in C++ zum Beispiel den Modifizierer __value.

Betrachten wir noch einmal die folgende Codezeile aus dem Abschnitt über Grundtypen:

System.Int32 a = new System.Int32(5); 


Beim Kompilieren dieser Anweisung erkennt der Compiler, dass es sich bei System.Int32 um einen Wertetyp handelt, und optimiert den resultierenden IL-Code so, dass dieses "Objekt" nicht auf der Halde angelegt wird, sondern auf dem Stapel des Threads in einer lokalen Variablen a.

Wenn möglich, sollten Sie Werttypen statt Referenztypen einsetzen, weil die Anwendung dann ganz einfach etwas schneller laufen wird. Insbesondere sollten Sie einen Typ als Werttyp definieren, wenn die folgenden Punkte zutreffen:

  • Der Typ dient als Grundtyp.

  • Der Typ braucht keine anderen Typen zu beerben.

  • Vom fraglichen Typ werden keine anderen Typen abgeleitet.

  • Objekte dieses Typs werden nicht oft als Argumente an Methoden übergeben. Das würde nämlich zu vielen Kopiervorgängen im Speicher führen und das Programm etwas bremsen. Der nächste Abschnitt über die neuen Verpackungskünste von .NET ("boxing" und "unboxing") erläutert dies noch etwas genauer.

Der wichtigste Vorteil eines Werttyps ist, dass er nicht auf der verwalteten Speicherhalde angelegt wird. Im Vergleich mit Referenztypen unterliegen Werttypen natürlich einigen Beschränkungen.

Für Werttypen gibt es zwei Repräsentationen, nämlich eine unverpackte Form (unboxed) und eine verpackte Form (boxed). Referenztypen sind ausschließlich boxed.

Werttypen werden implizit von System.ValueType abgeleitet. Dieser Typ bietet dieselben Methoden an wie System.Object. Allerdings überschreibt System.ValueType die Methode Equals, so dass sie true liefert, wenn die Werte in den Datenfeldern zweier Objektinstanzen übereinstimmen. Außerdem überschreibt System.ValueType die Methode GetHashCode mit einer Variante, die zur Berechnung des Hashcodes auch die Datenelemente der Instanz einbezieht. Wenn Sie Ihre eigenen Werttypen definieren, ist es also empfehlenswert, die Methoden Equals und GetHashCode entsprechend zu überschreiben.

Da man einen Werttyp nicht als Basisklasse für einen neuen Wert- oder Referenztyp verwenden kann, sollten Werttypen keine virtuellen Funktionen haben. Außerdem können sie nicht abstrakt sein. Und sie sind implizit versiegelt (sealed - ein versiegelter Typ kann nicht als Basis für einen neuen Typ dienen).

Variablen mit Referenztypen enthalten die Speicheradresse eines Objekts, das irgendwo auf der Halde liegt. Normalerweise wird eine Referenzvariable bei ihrer Entstehung mit null initialisiert, was nichts anderes bedeutet, als dass die Variable derzeit nicht auf ein gültiges Objekt verweist. Der versuchte Zugriff über einen Null-Referenztyp führt zur Meldung der Ausnahme NullReferenceException. Ein Werttyp enthält im Gegensatz dazu immer einen Wert des zugrundeliegenden Typs. Normalerweise werden alle Datenelemente im Werttyp mit null initialisiert. Es ist nicht möglich, durch den korrekten Zugriff auf einen Werttyp eine NullReferenceException-Ausnahme auszulösen.

Wenn Sie einer Werttypvariablen eine andere Werttypvariable zu weisen, wird eine Kopie des Werts angefertigt. Wenn Sie eine Referenztypvariable an eine andere Referenztypvariable zuweisen, wird dagegen nur die Speicheradresse kopiert. Anschließend verweisen die beiden Referenztypen auf dasselbe Objekt, das irgendwo auf der Halde liegt.

Es ist also durchaus möglich, dass zwei oder mehr Referenztypen auf dasselbe Objekt verweisen. Dadurch werden Operationen, die mit einer der beteiligten Variablen ausgeführt werden, auch in den anderen Variablen sichtbar. Werttypen haben dagegen ihre eigenen Kopien der Objektdaten und es kommt bei den zugelassenen Operationen nicht vor, dass eine Operation auf einen Werttyp einen anderen, nicht direkt beteiligten Werttyp beeinflusst.

Nun gibt es die seltenen Situationen, in denen die Laufzeitschicht einen Werttyp initialisieren muss und nicht in der Lage ist, dessen Standardkonstruktor aufzurufen. Das kann zum Beispiel geschehen, falls ein threadspezifischer Werttyp angelegt und initialisiert werden muss, wenn ein unverwalteter Thread erstmals verwalteten Code ausführt.

In dieser Situation kann die Laufzeitschicht zwar nicht den Konstruktor des Typs aufrufen, sorgt aber immerhin dafür, dass alle Datenelemente mit null initialisiert werden. Aus diesem Grund wird empfohlen, für Werttypen keine parameterlosen Konstruktoren zu definieren. Der C#-Compiler (und andere) sieht das sogar als Fehler an und weigert sich, den Code zu kompilieren. Allerdings ist das Problem recht selten und tritt nie bei Referenztypen auf. Für parametrisierte Konstruktoren gibt es keine derartigen Beschränkungen, weder bei Werttypen noch bei Referenztypen.

Da unverpackte (unboxed) Werttypen nicht auf der Halde angelegt werden, wird der von ihnen belegte Speicher sofort zurückgegeben, sobald die Methode nicht mehr aktiv ist, in der die fragliche Instanz liegt. Das bedeutet auch, dass Objekte eines unverpackten Werttyps keine Hinweisnachrichten erhalten, wenn ihr Speicher zurückgefordert wird. Dagegen wird im Zuge der Garbage Collection die Finalize-Methode eines verpackten (boxed) Werttyps aufgerufen. Es wird dringend davon abgeraten, einen Werttyp mit einer Finalize-Methode zu implementieren. Der C#-Compiler betrachtet dies wie einen parameterlosen Konstruktor als Fehler und weigert sich, den Code zu kompilieren.

Boxing und Unboxing

Es gibt viele Situationen, in denen es sehr bequem ist, einen Werttyp als Referenztyp zu behandeln. Nehmen wir an, Sie wollten ein ArrayList-Objekt anlegen (ein Typ, der im Namensraum System.Collections definiert wird), das eine Reihe von Points aufnehmen soll. Der Code könnte ungefähr so aussehen wie in Listing L2.

L2 Ein Beispiel für Boxing

// deklariere einen Werttyp 
struct Point { 
   public int x, y; 
} 
static void Main() { 
   ArrayList a = new ArrayList(); 
   for (int i = 0; i < 10; i++) { 
      Point p;                // lege einen Point an  
                              //   (nicht auf der Halde) 
      p.x = p.y = i;          // Initialisiere die Datenelemente 
                              //   der Instanz 
      a.Add(p);               // "verpacke" den Wert und trage die 
                              // Referenz ins Array ein. 
   } 
   . 
   . 
   . 
}

In jedem Schleifendurchlauf wird ein Point-Werttyp initialisiert. Anschließend wird der Point in der ArrayList gespeichert. Dieser Vorgang ist eine genauere Untersuchung wert. Was genau wird eigentlich in der ArrayList abgelegt? Ist es die Point-Struktur? Oder die Adresse der Point-Struktur? Oder etwas völlig anderes? Um diese Frage zu beantworten, müssen Sie in der Dokumentation die Add-Methode von ArrayList heraussuchen und sich anschauen, wie eigentlich der Parameter definiert ist. Der Prototyp der Add-Methode müsste ungefähr so aussehen (im Moment ändert sich ja noch vieles):

public virtual void Add(Object value) 


Wie aus diesem Prototyp hervorgeht, erwartet Add einfach ein Object als Argument. Object bezeichnet immer einen Referenztyp. In meinem Beispiel übergebe ich aber p. Und das ist ein Point-Wertetyp. Damit mein Beispiel funktioniert, muss der Point-Wertetyp in ein echtes Objekt auf der verwalteten Halde verwandelt und eine Referenz auf dieses Objekt ins Array eingetragen werden.

Die Umwandlung eines Wertetyps in einen Referenztyp nennt man Boxing. Verpacken. Intern geschieht folgendes, wenn ein Wertetyp eine Box erhält:

  1. Auf der Speicherhalde wird ein passender Speicherblock angelegt. Die erforderliche Größe richtet sich nach dem Speicherbedarf des Wertetyps. Zusätzlich ist noch etwas Platz für die Verwaltung erforderlich, die diesen Typ zu einem echten Objekt macht. Zu diesem Zweck muss noch ein Zeiger auf eine Tabelle mit den virtuellen Methoden untergebracht werden, sowie ein Zeiger auf einen Synchronisationsblock.

  2. Die Wertebits des Typs werden in den neu angelegten Speicherblock kopiert.

  3. Die Adresse des neuen Objekts wird an den Aufrufer zurückgegeben. Diese Adresse stellt nun praktisch einen Referenztyp dar.

Manche Sprachcompiler (zum Beispiel die für C#) generieren den IL-Code zwar automatisch, der für die Verpackung des Wertetyps erforderlich ist, aber man sollte trotzdem wissen, was unter der Haube vor sich geht. Nur dann kann man die Codegröße und Leistungsaspekte abschätzen.

Beim Aufruf der Add-Methode wird ein passender Speicherblock für das Point-Objekt von der verwalteten Halde angefordert. Die Datenelemente aus dem aktuellen Point-Wertetyp (p) werden in das neue Point-Objekt kopiert. Die Adresse des Point-Objekts (ein Referenztyp) wird ans Programm zurückgegeben und dann an die Add-Methode übergeben. Das Point-Objekt bleibt auf der Halde, bis es irgendwann vom Garbage Collector aufgesammelt wird. Nach der Rückkehr der Add-Methode kann die Point-Variable p wiederverwendet oder entsorgt werden. Die ArrayList bekommt es ja gar nicht erst in die Finger. Das "Boxing" ermöglicht eine einheitliche Sicht auf das Typsystem, wobei sich ein Wert von jedem beliebigen Typ letztlich als Objekt betrachten lässt.

Das Gegenteil vom Verpacken ist natürlich das Auspacken (Unboxing). Ausgepackt wird eine Referenz auf den Wertetyp (die Datenfelder), die im Objekt enthalten sind. Intern geschieht beim Auspacken eines Referenztyps folgendes:

  1. Die gemeinsame Laufzeitschicht überprüft zuerst, ob die Referenztypvariable ungleich null ist und auf ein Objekt verweist, bei dem es sich um einen verpackten Wert des gewünschten Wertetyps handelt. Schlägt einer dieser Tests fehl, wird eine InvalidCastException-Ausnahme gemeldet.

  2. Sofern der Typ passt, wird ein Zeiger auf den Wertetyp zurückgegeben, der im Objekt liegt. Der Wertetyp, auf den sich dieser Zeiger bezieht, enthält nicht den üblichen Verwaltungsaufwand, der mit einem echten Objekt verbunden ist (ein Zeiger auf eine Tabelle mit den virtuellen Methoden und auf einen Synchronisationsblock.)

Beim Verpacken (Boxing) entsteht immer ein neues Objekt und die Bits des unverpackten Wertetyps werden in das neue Objekt kopiert. Beim Auspacken (Unboxing) erhalten Sie dagegen einfach nur einen Zeiger auf die Daten, die im verpackten Objekt liegen. Es findet also kein aufwendiger Kopiervorgang statt. Allerdings sorgt der übliche Code im Normalfall sowieso dafür, dass die Daten, die über die ausgepackte Referenz zugänglich sind, in irgendeiner Form kopiert werden.

Die folgenden Zeilen sollen verdeutlichen, was beim Boxing und Unboxing geschieht:

public static void Main() { 
   Int32 v = 5;    // Lege eine unverpackte Wertetyp-Variable an 
   Object o = v;   // o bezieht sich auf die verpackte Version von v 
   v = 123;        // ändert den unverpackten Wert auf 123 
   Console.WriteLine(v + ", " + (Int32) o);    // Zeigt "123, 5" 
}


Können Sie nun abschätzen, wie viele Verpackungsoperationen in diesem Code auftreten? Sie werden überrascht sein! Es sind nämlich drei. Schauen wir uns den Code genau an und überlegen, was eigentlich geschieht.

Zuerst wird ein unverpackter Int32-Wertetyp (v) angelegt und mit 5 initialisiert. Dann baut der Code einen Object-Referenztyp (o) zusammen und soll eigentlich auf v verweisen. Aber Referenztypen müssen immer auf Objekte verweisen, die auf der Halde liegen. Also generiert der C#-Compiler den passenden IL-Code, der v standesgemäß verpackt, und speichert die Adresse der verpackten Version von v in o ab. Nun wird die Zahl 123 ausgepackt und die referenzierten Daten werden in den unverpackten Wertetyp v kopiert. Das hat übrigens keinen Einfluss auf die verpackte Version von v, die somit ihren Wert 5 behält. Dieses Beispiel zeigt, wie o ausgepackt wird (das führt zu einem Zeiger auf die Daten in o) und die Daten aus o dann in den unverpackten Wertetyp v kopiert werden.

Nun soll der Aufruf von WriteLine erfolgen. WriteLine erwartet ein String-Objekt als Argument. Sie haben aber kein String-Objekt, sondern nur diese drei Gegenstände: einen unverpackten Int32-Wertetyp (v), einen String und einen Int32-Referenztyp (o), der als Verpackung entstanden ist. Irgendwie muss daraus nun das erforderliche String-Objekt entstehen.

Um das zu erreichen, generiert der C#-Compiler den Code für den Aufruf der statischen Methode Concat des String-Objekts. Wie Sie sicher schon ahnen, gibt es von Concat eine ganze Reihe von überladenen Versionen. Alle liefern letztlich dasselbe Ergebnis. Sie unterscheiden sich bloß in der Zahl und Art der Parameter. Da Sie den String aus drei Teilen formatieren möchten, wählt der Compiler die folgende Version von Concat:

public static String Concat(Object arg0, Object arg1, Object arg2);


Für den ersten Parameter arg0 wird v übergeben. Aber v ist eine unverpackter Wertetyp und arg0 ist ein Object. Also muss v verpackt werden und die Adresse vom verpackten v wird für arg0 an Concat weitergegeben. Für den Parameter arg1 wird die Adresse des Strings ", " übergeben, also die Adresse eines String-Objekts. Und für den Parameter arg2 schließlich wird o (eine Referenz auf ein Object) in ein Int32 umgewandelt. Dadurch entsteht ein temporärer Int32-Wertetyp, der die unverpackte Version des Werts erhält, auf den sich o derzeit bezieht. Natürlich muss dieser temporäre Int32-Wertetyp wieder verpackt werden. Das Ergebnis ist wieder eine Speicheradresse, die an den Parameter arg2 von Concat weitergegeben wird.

Nach ihrem Aufruf ruft Concat die ToString-Methoden jedes angegebenen Objekts auf und verknüpft die Stringdarstellungen der einzelnen Objekte. Das String-Objekt, das Concat an den Aufrufer zurückgibt, wird direkt an WriteLine weitergegeben, damit das Ergebnis auf dem Bildschirm erscheint.

Ich sollte vielleicht noch darauf hinweisen, dass der generierte IL-Code effizienter ist, wenn man den WriteLine-Aufruf folgendermaßen schreibt:

Console.WriteLine(v + ", " + o);    // Zeigt "123, 5"


Diese Zeile ähnelt der vorigen Version, mit einem kleinen Unterschied: ich habe die (Int32)-Typkonvertierung gelöscht, die vor der Variablen o stand. Dieser Code ist nun effizienter, weil es sich bei o bereits um einen Referenztyp auf ein Object handelt, dessen Adresse einfach an die Concat-Methode weitergegeben werden kann. Die Entfernung der Typkonvertierung erspart also einen Auspackvorgang und die Neuverpackung des Wertetyps. Hier ein weiteres Beispiel für Boxing und Unboxing:

public static void Main() { 
   Int32 v = 5;          // lege eine unverpackte Wertetyp-Variable an 
   Object o = v;         // o bezieht sich auf die verpackte Version  
                         //   von v 
   v = 123;              // ändere unverpackten Wertetyp auf 123 
   Console.WriteLine(v); // Zeigt "123" 
   v = (Int32) o;        // auspacken, in v ablegen 
   Console.WriteLine(v); // Zeigt "5" 
}


Wie viele Verpackungsorgien zählen Sie in diesem Code? Die Antwort ist: eine. Es gibt nur einen einzigen Verpackungsvorgang, weil es eine WriteLine-Methode gibt, die ein Int32 als Argument annimmt:

public static void WriteLine(Int32 value);


In den beiden Aufrufen von WriteLine wird die Variable v (ein unverpackter Int32-Wertetyp) als Wert übergeben. Nun mag es zwar sein, dass WriteLine diesen Int32-Wert intern wieder verpackt, aber darüber hat man als Entwickler sowieso keine Kontrolle. Wichtig ist, dass Sie sich redlich bemüht haben und den eigenen Code weitgehend Boxing-frei gestaltet haben.

Wenn Sie genau wissen, dass der Compiler für den Code, den Sie gerade entwickeln, eine Menge Verpackungscode generieren wird, erhalten Sie ein kleineres und schnelleres Ergebnis, wenn Sie ihm die Arbeit abnehmen und die Wertetypen von Hand verpacken (Listing L3).

L3 Von Hand verpackte Wertetypen

public static void Main() { 
   Int32 v = 5;         // lege eine unverpackte Wertetyp-Variable an 
   // Beim Kompilieren der folgenden Zeilen wird v dreimal verpackt. 
   // Eine Verschwendung von Speicher und CPU-Zeit... 
   Console.WriteLine(v + ", " + v + ", " + v); 
   // die folgenden Zeilen verbrauchen weniger Speicher und laufen 
   // schneller 
   Object o = v;           // verpacke v von Hand (genau einmal) 
   // für die folgende Zeile ist keine zusätzliche Verpackung 
   // erforderlich 
   Console.WriteLine(o + ", " + o + ", " + o); 
}


Der C#-Compiler erzeugt den Code zum Verpacken und Auspacken automatisch. Das vereinfacht zwar die Programmierung, verbirgt den Aufwand aber auch vor solchen Entwicklern, die um eine möglichst hohe Leistung bemüht sind. Wie C# können auch andere Programmiersprachen die Einzelheiten über die Verpackung verbergen. Manche Sprachen zwingen den Programmierer aber, den entsprechenden Code zum Verpacken explizit zu schreiben. In C++ mit den Managed Extensions zum Beispiel ist es erforderlich, Wertetypen explizit mit dem Operator __box zu verpacken. Das Auspacken von Wertetypen geschieht durch die Konvertierung des verpackten Typs ins unverpackte Äquivalent mit dynamic_cast.

Eine letzte Bemerkung: wenn ein Wertetyp eine virtuelle Methode nicht überschreibt, die von System.ValueType definiert wird, lässt sich diese Methode nur in der verpackten Form des Wertetyps aufrufen. Das liegt einfach daran, dass nur die verpackte Form des Objekts einen Zeiger auf die Tabelle mit den virtuellen Methoden hat. Dagegen lassen sich Methoden, die direkt mit dem Wertetyp definiert werden, in den verpackten und unverpackten Versionen des Werts aufrufen.

Fazit

Die in dieser Kolumne besprochenen Konzepte sind für alle .NET-Entwickler äußerst wichtig. Sie sollten den Unterschied zwischen Wertetypen und Referenztypen noch im Tiefschlaf verstehen. Außerdem müssen Sie wissen, welche Operationen eine spezielle Verpackung (Boxing) erfordern. Und wenn Sie mit einem Compiler arbeiten, der diese Verpackungsvorgänge automatisch erledigt (wie C# und Visual Basic), sollten Sie auch erkennen können, wann der Compiler das tun wird und welche Auswirkung das auf Ihren Code hat. Ich kann gar nicht oft genug wiederholen, dass eine Fehlinterpretation dieser Konzepte leicht zu subtilen Programmierfehlern und langsameren Programmen führt.

Anzeigen:
© 2014 Microsoft