C# har to slags typer: referencetyper og værdityper implementeret via henholdsvis class og struct. Referencetyper bliver altid allokeret på heapen, og variabler af denne type er, som navnet antyder, en reference til de egentlige værdier. Værdityper allokeres på stakken, og variabler af denne type, indeholder de egentlige værdier, så indholdet kan tilgås direkte. Det vil sige, at referencetyper er dyrere at bruge i mange tilfælde, og derfor tilbyder C# værdityper som et billigere alternativ. I C# er det altså typedefinitionen, der afgør om instanser af en given type placeres på stakken eller heapen og ikke, som det kendes fra f.eks. C++, brugen af den aktuelle type.
Eftersom at man kan have brug for at opfatte værdityper som referencetyper og omvendt, tilbyder C# et sæt mekanismer kaldet boxing og unboxing til dette formål. Referencetyper siges at være boxed, mens værdityper er unboxed. At konvertere en værditype til en referencetype kaldes derfor boxing, mens en konvertering fra en referencetype til en værditype kaldes unboxing.
I de fleste tilfælde sørger compileren for at generere de nødvendige box/unbox instruktioner, når der er behov for det (i enkelte tilfælde skal compileren have lidt hjælp i form af casting). Et oplagt tilfælde er, når man ønsker at kalde en metode, der forventer en referencetype med en værditype. Her sørger compileren for al det nødvendige, så i dette tilfælde:
// DateTime er en værditype
DateTime datetime = new DateTime(1999, 12, 31);
Console.WriteLine(datetime);
sørger compileren for at lave en boxed version af datetime, da Console.WriteLine() ikke accepterer instanser af DateTime. Den resulterende IL ser således ud:
.maxstack 4
.locals init ([0] valuetype [mscorlib]System.DateTime datetime)
...
IL_0001: ldloca.s datetime
IL_0003: ldc.i4 0x7cf
IL_0008: ldc.i4.s 12
IL_000a: ldc.i4.s 31
IL_000c: call instance void [mscorlib]System.DateTime::.ctor(int32,
int32,
int32)
IL_0011: nop
IL_0012: ldloc.0
IL_0013: box [mscorlib]System.DateTime
IL_0018: call void [mscorlib]System.Console::WriteLine(object)
Instruktionerne 0012 og 0013 tager stakvariablen med indeks 0 og kalder box til typen System.DateTime, inden Console::WriteLine() kaldes i 0018.
Unboxing anvendes som nævnt, når man ønsker at konvertere en referencetype til en værditype. Før .NET 2.0 skete dette ofte, når man skulle have værdityper ud af diverse collections, der alle rummede referencetyper i form af object-referencer. Med generic collections er dette ikke længere nødvendigt, men der vil stadig være tilfælde, hvor man har brug for at konvertere få en værditype ud af en referencetype.
Da boxing/unboxing er noget, der sker automatisk i de fleste tilfælde, er det ikke sikkert at alle tænker over, hvad der egentlig foregår under kølerhjelmen, hvilket kan lede til overraskende episoder som nedenstående eksempel illustrerer.
Spørgsmålet er, hvad udskriver nedenstående?
class Program {
static void Main(string[] args) {
Position pos = new Position(10, 10);
Console.WriteLine(pos);
object o = pos;
((Position)o).Move(5, 5);
Console.WriteLine(o);
}
}
public struct Position {
private int X;
private int Y;
public Position(int x, int y) {
X = x;
Y = y;
}
public void Move(int deltax, int deltay) {
X += deltax;
Y += deltay;
}
public override string ToString() {
return string.Format("({0}, {1})", X, Y);
}
}
Det nærliggende svar er desværre ikke det korrekte. Positionen (10, 10) udskrives i begge tilfælde.
Programmet er forsimplet for eksemplets skyld, men situationen er bestemt ikke utænkelig. Vi har en værditype, som vi har brug for at referere via en referencetype. Senere i forløbet har vi brug for at kalde en af Positions metoder på vores boxed instans, men da object ikke kender Position, er vi nødt til at hjælpe compileren lidt på vej ved at fortælle den, at vi ved bedre – o er ikke blot et object, det peger faktisk på en Position. (Kalder vi GetType() på o, får vi ligeledes at vide, at o peger på en instans af typen Position).
Så hvorfor virker ovenstående ikke som forventet? Lad os se nærmere på IL-koden,
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 57 (0x39)
.maxstack 3
.locals init ([0] valuetype ConsoleApplication.Position pos,
[1] object o,
[2] valuetype ConsoleApplication.Position CS$0$0000)
IL_0000: nop
IL_0001: ldloca.s pos
IL_0003: ldc.i4.s 10
IL_0005: ldc.i4.s 10
IL_0007: call instance void ConsoleApplication.Position::.ctor(int32,
int32)
IL_000c: nop
IL_000d: ldloc.0
IL_000e: box ConsoleApplication.Position
IL_0013: call void [mscorlib]System.Console::WriteLine(object)
IL_0018: nop
IL_0019: ldloc.0
IL_001a: box ConsoleApplication.Position
IL_001f: stloc.1
IL_0020: ldloc.1
IL_0021: unbox.any ConsoleApplication.Position
IL_0026: stloc.2
IL_0027: ldloca.s CS$0$0000
IL_0029: ldc.i4.5
IL_002a: ldc.i4.5
IL_002b: call instance void ConsoleApplication.Position::Move(int32,
int32)
IL_0030: nop
IL_0031: ldloc.1
IL_0032: call void [mscorlib]System.Console::WriteLine(object)
IL_0037: nop
IL_0038: ret
} // end of method Program::Main
Læg mærke til, at der er tre lokale variable på stakken: indeks 0 er vores pos, indeks 1 er vores o, og så er der en lokal arbejdsvariabel af typen Position med indexs 2. Instruktionerne 0019 til 001f indlæser pos, boxer den og gemmer den boxed version i vores lokale variable med indeks 1 (altså o). box-instruktionen dækker desuden over, at der er oprettet et objekt på heapen, og indholdet af pos er kopieret til dette objekt. o peger på denne instans.
Så hvad sker der som forberedelse til vores kald af Move()? Instruktion 0020 tager fat i o, unboxer den til en Position og gemmer resultatet i vores lokale arbejdsvariabel med indeks 2. unbox-operationen kopier indholdet af den boxed udgave, som o peger på, til den lokale arbejdskopi og kalder Move() på denne version! Det vil sige, at Move() ændrer indholdet af denne midlertidige valuetype og ikke det, som o peger på, og derfor giver ovenstående ikke det forventede resultat.
Problemet opstår som følge af kombinationen boxing/unboxing og metoder, der ændrer indholdet af en værditype. Da vi næppe kan gøre noget ved eksistensen af boxing/unboxing-mekanismerne, er den eneste løsning at lave værdityper, der er immutable. Det vil sige, enhver ændring af en værditype skal i så fald returnere en ny instans og ikke som i ovenstående modificere indholdet af den eksisterende. For at lave Position immutable, skal vi ændre Move() som følger:
public Position Move(int deltax, int deltay) {
return new Position(X + deltax, Y + deltay);
}
Kaldet til Move() skal ligeledes ændres, så o kommer til at pege på den returnerede værdi:
o = ((Position)o).Move(5, 5);
Ovenstående virker efter hensigten, men nu skal der faktisk endnu flere knæbøjninger til at skabe det ønskede resultat, som vi kan se af den genererede kode:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 62 (0x3e)
.maxstack 3
.locals init ([0] valuetype ConsoleApplication.Position pos,
[1] object o,
[2] valuetype ConsoleApplication.Position CS$0$0000)
IL_0000: nop
IL_0001: ldloca.s pos
IL_0003: ldc.i4.s 10
IL_0005: ldc.i4.s 10
IL_0007: call instance void ConsoleApplication.Position::.ctor(int32,
int32)
IL_000c: nop
IL_000d: ldloc.0
IL_000e: box ConsoleApplication.Position
IL_0013: call void [mscorlib]System.Console::WriteLine(object)
IL_0018: nop
IL_0019: ldloc.0
IL_001a: box ConsoleApplication.Position
IL_001f: stloc.1
IL_0020: ldloc.1
IL_0021: unbox.any ConsoleApplication.Position
IL_0026: stloc.2
IL_0027: ldloca.s CS$0$0000
IL_0029: ldc.i4.5
IL_002a: ldc.i4.5
IL_002b: call instance valuetype ConsoleApplication.Position
ConsoleApplication.Position::Move(int32, int32)
IL_0030: box ConsoleApplication.Position
IL_0035: stloc.1
IL_0036: ldloc.1
IL_0037: call void [mscorlib]System.Console::WriteLine(object)
IL_003c: nop
IL_003d: ret
} // end of method Program::Main
Som før unboxer vi o og kalder Move() på denne midlertidige udgave, men nu får vi tilmed en ny instans af Position, som vi sluttelig boxer, og lader o pege på. Det betyder yderligere en kopiering af Positions indhold i forhold til før (men hvad gør man ikke for at få kode, der faktisk virker efter hensigten).
Nu er Position ikke særlig omfattende, men det siger sig selv, at for komplekse typer er boxing/unboxing en krævende operation, så man bør være klar over, at tilsyneladende uskyldige operationer kan medføre en hulens masse ekstra arbejde. Det er derfor ikke helt ligegyldig, om man implementerer en given type som referenfcetype eller værditype, og værdityper bør som sagt implementeres, så de er immutable.