Archive for the ‘Boxing/unboxing’ Category

String, Int32 og implicit konvertering eller mangel på samme

Friday, February 6th, 2009

Inspireret af et fint, lille spørgsmål på Stack Overflow skal vi i dette indlæg se lidt nærmere på String, sammensætninger og implicit konvertering. Spørgeren undrede sig nemlig over, at man kan skrive

string answer = "the answer is " + 42;

men ikke

string doesntwork = 1;

Forsøger vi os med sidstnævnte, fortæller compileren os prompte, at der ikke findes en implicit konvertering fra Int32 til String. Men hvad sker der så i det første tilfælde? Hvordan får vi oversat tallet 42 til en String?

Der kom et par uklare svar på spørgsmålet, men det er i realiteten ikke så svært at finde ud af, hvad der sker, men inden vi kommer så langt, vil jeg lige tilføje et lille twist til problemet. Hvad bliver indholdet af answer, hvis vi i stedet skriver

string answer = "the answer is " + 4 + 2;

Svaret er naturligvis stadig 42 (det skal det jo være!), men hvorfor nu det?

Årsagen skal findes i hvordan C# compileren behandler sammensætning af strings. Ovenstående oversættes nemlig ikke ved hjælp kald af plusoperatoren, for den er slet ikke defineret for String. I stedet bliver string1 + string2 oversat til kald af en passende udgave af den statiske metode ConcatString

Det vil sige, at i første tilfælde får vi kaldt Concat(object, object) og i anden udgave Concat(object, object, object). Det betyder ydermere, at i første tilfælde bliver værdien 42 boxed til en Int32, mens vi i andet tilfælde kalder metoden med boxed versioner af 4 og 2

Kigger vi på implementeringen af Concat for object-varianterne, ser vi, at den ender med at kalde ToString() på de enkelte instanser for derefter at kalde den passende Concat-variant for Strings

Der er således ikke tale om implicit konvertering fra Int32 til String, men derimod boxing til Int32 efterfulgt af kald til ToString(), der jo som bekendt arves fra Object. Således hænger det sammen, at compileren i visse tilfælde tillader og i andre tilfælde nægter at oversætte tal til tekst for os. 

Uheldige låse

Monday, August 18th, 2008

I mit indlæg om tråde og samtidighedsproblemer skrev jeg lidt om valg af låse til udelt adgang, men der var ikke plads til at komme ind på alle detaljerne. Derfor vil jeg i dette indlæg se lidt nærmere på problematikken.

I C# er udelt adgang implementeret via Monitor-klassen. Metoden Enter(object) tager en instans af en vilkårlig referencetype og bruger denne som lås. Er låsen ledig, får tråden lov til at fortsætte og er derefter sikret udelt adgang til den efterfølgende kode. Er låsen optaget af en anden tråd, bliver den spørgende tråd lagt til at vente, indtil låsen igen er ledig. En lås frigives igen ved at kalde Monitor.Exit(). Der er flere varianter, men det er uvæsentlig for denne diskussion.

Glemmer vi at frigive en lås, kan det lede til alvorlige samtidighedsproblemer, og derfor er det vigtig, at låse også frigives i tilfælde af fejl. Bruger vi Monitor.Enter() og Exit(), skal sidstnævnte således altid kaldes fra en finally-blok. For at gøre det lettere at skrive korrekt kode tilbyder C# som bekendt et lock keyword, der netop implementerer denne fremgangsmåde.

Med denne korte introduktion bag os kan vi nu rette blikket i retning af, hvad der kan og bør låses på. Da Enter() tager en instans af typen object, kan vi jo låse på hvad som helst, men i praksis er der instanser, der er meget uheldige at bruge som låse. Her er en oversigt.

Offentlige instanser: Når vi implementerer udelt adgang, er det nødvendig, at alle tilgange til de resourcer, vi ønsker at beskytte, foregår via en den samme lås. Det er ligeledes vigtig, at vi har fuldstændig styr på, hvor låse bliver tilegnet og frigivet. Derfor giver det næsten sig selv, at det er en dårlig ide, at låse på en instans, der er offentlig tilgængelig, da det jo lukker op for, at andre kan låse på denne instans og dermed introducere samtidighedsproblemer for vores kode.

this: Microsofts kode gør det flere steder, men det ændrer ikke ved, at det er en uskik at bruge lock(this). this er jo netop den ultimative offentlige variabel. Uanset hvad vi gør, kan vi ikke beskytte adgangen til denne, når der først eksisterer instanser af typen.

typeof(): Ligesom this er typeof tilgængelig både i og udenfor vores kode, men hvad værre er, så vil typeof() i nogle tilfælde returnere den samme instans på tværs af AppDomains! Det vil sige, at tråde i et AppDomain kan påvirke tråde i et andet. Det er rigtig skidt, og den slags fejl kan være meget svære at identificere. Hvis et assembly er indlæst som domain neutral, vil alle AppDomains dele typerne i dette assembly og dermed få samme reference som svar på et kald til typeof(). Som udgangspunkt er det kun mscorlib.dll, der indlæses således, så f.eks. vil typeof(int) returnere en reference til samme instans på tværs af AppDomains. Via konfiguration kan vi indlæse andre assemblies, så de bliver delt på denne måde, og dermed vil typeof() give samme problem for typer i disse assemblies.

string: Som jeg tidligere har omtalt, har CLRen en mekanisme kaldet interning, hvis formål er at forsøge at reducere antallet af strings i hukommelsen ved at lade strings med identisk indhold være repræsenteret af referencer til samme instans. Det betyder naturligvis, at vi kan erklære en string private i håb om at ingen andre kan tilgå den, men CLRen vil ikke desto mindre returnerer referencen til præcis denne string, hvis den optræder andre steder i koden. Faktisk er interned strings delt på tværs af ikke bare klasser men også AppDomains, hvilket gør det risikabelt at bruge strings som låse.

Komplekse typer: Der burde ikke være noget galt i at låse på en instans af en vilkårlig privat type, men hvis denne type følger den uheldige praksis at låse på this, kan vores kode føre til deadlocks, hvis vi låser på vores instans. Derfor er det vigtig at være opmærksom på, om typen anvender låse eller ej, inden man låser på en instans.

Værdityper (value types): Forsøger vi at kalde lock på en værditype, får vi en compile-fejl, men kalder vi Monitor.Enter() direkte, er compileren flink nok til at boxe vores værditype. Desværre betyder det jo, at forskellige kald til Enter() vil få hver deres boxed kopi af vores værditype. Heldigvis vil vi dog få en exception, når vi efterfølgende forsøger at kalde Exit() på den samme værdi, idet vi jo forsøger at frigive en lås på endnu en boxed udgave af vores værdi.

Jeg skal ikke afvise, at der er en enkel eller to problematiske konstruktioner, jeg har overset, men i alle tilfælde vidner ovenstående om, at vi ikke bare skal låse på en vilkårlig instans.

Anbefalingen er derfor: Opret en privat instans af object for hvert selvstændig område, hvor udelt adgang er påkrævet og lås på denne. Det er en god ide, at vedtage en navnestandard for disse låseobjekter, så de let kan identificeres.

Boxing, Unboxing og overraskelser

Wednesday, May 2nd, 2007

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()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.