A System.Enum egy referencia típus?!

Nemrégiben egy fiatal kollegám hívta fel a figyelmem arra, hogy az amúgy kényelmes System.Enum.HasFlag() metódus használata boxing-gal jár, legalábbis ezt olvasta a CLR via C# könyvben. Nagyon gyanús volt ez nekem, ezért jobban utána néztem a kérdésnek.

Miért gyanús itt a boxing?

Szépen megtanultuk, hogy a System.ValueType-ból származó típusok értéktípusok .NET alatt. A System.Enum a System.ValueType-ból származik, így elvileg értéktípusú, mellesleg tudjuk, hogy a C# enum kulcsszóval leírt típusokra a C# compiler egy System.Enum-ból származó típust generál, és ez szintén értéktípusú.

Mivel a System.Enum.HasFlag() paramétere egy System.Enum, ami elvileg nem referencia típus, nem kell boxing.

Mi a valóság?

A kérdést könnyen eldönthetjük egy példaprogram segítségével:

using System;

enum alma { piros, zold };

class Program
{
    static void Main(string[] args)
    {
        var a = alma.piros;

        if (a.HasFlag(alma.piros))
        {
            Console.WriteLine("piros");
        }
    }
}

Mivel a fenti program használja a HasFlag metódust, az IL kódban meg kell találnunk a boxingot a CLR via C# állítása alapján:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       32 (0x20)
  .maxstack  2

// egy lokális változó van: alma típusú. A szögletes zárójelben levő szám érdekes, mivel a
// lokális változók egy "local variable array"-ben vannak, és a szám az array indexe. Később
// a kód ezzel hivatkozik:
  .locals init ([0] valuetype ConsoleApplication4.alma a)

  // var a = alma.piros:
  IL_0000:  ldc.i4.0  // a piros értéke nulla, az alma típus mögött int32 van, ezért "i4" típusú nulla a veremre
  IL_0001:  stloc.0   // a vermen levő érték tárolása a local variable array 0-ik indexén

  // a.HasFlag() hívása:
  IL_0002:  ldloc.0   // az "a" változó a veremre
  IL_0003:  box        ConsoleApplication4.alma  // boxing!!!

  IL_0008:  ldc.i4.0   // a "piros" érték a veremre
  IL_0009:  box        ConsoleApplication4.alma  // boxing!!!

// Most már a vermen van egy referencia típusú "this", a vermen van a paraméter, lehet hívni a függvényt:
  IL_000e:  call       instance bool [mscorlib]System.Enum::HasFlag(class [mscorlib]System.Enum)

// Ha hamis, ugorja át a kiírást:
  IL_0013:  brfalse.s  IL_001f

// Console.WriteLine("piros");
  IL_0015:  ldstr      "piros"
  IL_001a:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_001f:  ret
} // end of method Program::Main

Mi történt?

Nem kell érteni az IL kódot ahhoz, hogy látszódjon, nem egy, hanem két boxing művelet is történik. Hogy lehet ez?

Az első boxing

Bár a megoldás egyszerű, nagyon sokáig kellett néznem a kódot, mire rájöttem: az első boxing a this pointert készíti elő a metódushíváshoz. Hogy miért nagyon furcsa ez az egész? Sokan tudhatják, hogy az osztályok metódusai egy rejtett this paramétert tartalmaznak. Ennek a this paraméternek az értéke az az objektum, amire a metódus meg lett hívva. Ennek okai részletesen le vannak írva az értéktípusokról szóló cikkben. Ebben a cikkben az is megtalálható, hogy az értéktípusok és a referencia típusok memóriabeli lenyomata más. A referencia típusoknak nem egyértelmű a futás közbeni típusa (hivatkozhatok egy object típusú referencián keresztül egy Alma típusú példányra), emiatt a típusinformációt hordoznia kell a példánynak. Emiatt ha van egy egyszerű osztályom:

class Alma
{
  int szinkod;
  int meretkod;
}

Akkor ennek a memóriabeli lenyomata a következő lesz:

Ezzel szemben, ha értéktípusról van szó, annak a típusa mindig pontosan kiderül a környezetből, emiatt ez nem hordozza külön a típusinformációt:

Miért érdekes ez most? Azért, mert ha van egy referencia típusom, és annak egy metódusa, akkor a metódus kódját úgy készíti el a fordító, hogy számít rá, hogy át kell ugrania a type handle-t (máshol type object pointernek vagy metódus tábla pointernek hívják) Ha ez a metódus a szinkod mezőt használja, akkor a generált kód ezt a 4. (32-bites rendszer esetén) offseten fogja keresni. Ez azt is jelenti, hogy ha egy érték típus örököl egy metódust egy referencia típusú őstől, akkor annak a meghívása nem lesz triviális. Azért nem, mert a referencia típusnál definiált függvény más felépítésű this paramétert vár, mint amit az azóta értéktípussá alakult példány adni tud. Az Object.ToString() úgy generálódott például, hogy átugorja a type handle-t. Ha készítek egy Alma értéktípust, és hívom a felül nem definiált ToString()-et, ez a ToString() nem tudja kezelni az Alma memória lenyomatát.

Ha az ős osztály függvényének, ami referencia típushoz lett generálva, odadobjuk az értéktípus “referenciáját”, akkor az például hibásan elkezdi átugrani a TypeHandle-t, hogy elérje az adott mezőt. Hogy ez ne történjen meg, ilyenkor az értéktípus egy referencia típusokra jellemző keretet kap, és ez lesz this pointerként átadva. A kerettel ellátott memória lenyomat a boxingolt értéktípus.

Ha egy értéktípusra például GetType()-ot hívunk, vagy nem írjuk felül a ToString() metódust és azt meghívjuk, ez a boxing meg fog történni.

Ami a fenti IL kódban nagyon furcsa, az az, hogy az értéktípusú “a” változót az IL kód boxingolja, hogy egy megfelelő this pointer tudjon átadni a HasFlag() metódusnak, tehát a HashFlag() metódus egy referencia típus metódusa, tehát a System.Enum-ot a C# fordító referencia típusként kezeli!

Miután ez kitisztult, megnéztem az MSDN-t, ahol egyértelműen látszik, hogy mi érték és mi referencia típus, mivel structure vagy class szavakat használ. És lám, Enum class!

Na de miért ez a trükközés?

Természetesen merül fel a kérdés, hogy miért van ennyire elbonyolítva ez a része a .NET-nek? A helyzet az, hogy mélyebben belegondolva nehéz máshogy elképzelni. A problémát az okozza, hogy az értéktípusok nem hordoznak típusinformációt magukkal, mivel a típus kiderül a környezetből (pl lokális változók esetén a metódus metaadataiból, osztály mező esetén az osztály metaadataiból, stb).

Csakhogy az Enum megengedi, hogy megadjuk, hogy a felsorolt értékeink mögött milyen típus legyen. Alap esetben ez egy 32 bites int, de tehetek ilyet:

enum alma:short { piros, zold };

Ekkor már nem 32 bit lesz a memória lenyomat. Az Enum.HasFlag-et, ha értéktípusról van szó, úgy kellene generálni, hogy a függvény implementációja tisztában legyen a this paraméter memória lenyomatával. Viszont ez lehet 32 bit, lehet 16 bit, attól függ, hogy mit adunk meg a C# enum kulcsszó mögé, Emiatt a System.Enum.HasFlag() nem tud sima értéktípusú this pointert fogadni, és emiatt a System.Enum nem tud értéktípus lenni. Azzal, hogy a System.Enum referencia típus, a this pointert uniform módon tudja átvenni, akár mit adunk meg az enum alma: mögé. Viszont így szükség van a boxing-ra. Ezen kívül a HasFlag() a belső implementációjában egyéb rondaságokat is művel, mire megszerzi (object-ként!) azt az értéket amit vizsgálni kell, de ebbe már nem érdemes belemenni (meg utána sem néztem pontosan)

Második boxing

A fordított kód két boxingot tartalmaz, amiből az elsőt megfejtettük. A második már egyenes következménye annak, hogy a System.Enum referencia típus, Mivel így referencia típusú paramétert vár a HasFlag(), az alma.piros értéktípusú “példányt” boxingolni kell.

Konklúzió

Ennek a történetnek két konklúziója van. Sokadszor derül ki számomra, hogy nem lehet elég mélyen ismerni a .NET rendszert, emiatt óvatosan kell vitatkozni egyszerűnek tűnő témákban is. A másik, hogy elvileg a HasFlag() használata sokkal lassabb, mint a régi, operátorokkal bitvadászó vizsgálat. Hogy ez kinek mennyire számít, mindenki eldöntheti maga. Én biztos, hogy két boxing miatt nem fogom mellőzni.

  1. #1 by mjanoska on 2011. October 14. - 11:33

    LOL! A tanulsággal egyetértek … persze jó lenne, ha a C# language Spec írói is így értették volna:
    “An enum type is a distinct value type with a set of named constants.”

    E mellett ugyanabban a doksiban – ami igazolja, hogy tényleg úgy gondolták:

    “4.3.1 Boxing conversions
    A boxing conversion permits a value-type to be implicitly converted to a reference-type. The following boxing conversions exist:

    • From any enum-type to the type System.Enum.
    • From any nullable-type with an underlying enum-type to the type System.Enum.
    …”

  2. #2 by Tóth Viktor on 2011. October 14. - 13:52

    🙂 Igen. Mondjuk a 8.2.4 pont alatt (így utólag olvasva) végülis leírja, hogy mi micsoda:

    “A boxed type cannot be directly referred to by name… …The closest named base class to a boxed enumerated value type is System.Enum; for all other value types it is System.ValueType. Fields typed System.ValueType can only contain the null value or an instance of a boxed value type. Locals typed System.Enum can only contain the null value or an instance of a boxed enumeration type.”

    De ha valaki végig is olvasta a specifikációt, szerintem ezt a részt csak az autisták jegyezték meg🙂

  3. #3 by kbatyai on 2011. October 21. - 13:09

    Oké, oké. De mihez is kell ennyire “mélyen ismerni a .NET rendszert”?!?
    Általánosságban🙂 Általános hétköznapi és nem valami speciális (durva performace, durva hekkeléses) esetet nézve…

  4. #4 by Tóth Viktor on 2011. October 21. - 14:05

    Nem kell ismerni a programozási feladatok legtöbbjéhez ilyen szinten. Ez inkább olyan, mint amikor öt lépést boncolgatnak oldalakon keresztül egy érdekesebb sakkjátszmából. Nincs gyakorlati haszna, de valakit érdekel agytornáztatásnak. Én meg azért írtam le, hogy hátha mást is, nem csak engem🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: