IL Assembly: wywoływanie metod wirtualnych na typach generycznych

Dzisiaj kolejny wpis o generics internals. Wiemy już jak zostały one zaimplementowane w .NET i czym różni się generics z typem referencyjnym od value.  Kolejny etap to zapoznanie się z OpCodes.Constrained. Zobaczmy następujący kod:

class CustomStack<T>
{
   public void DoSomething(T value)
   {
       string str=value.ToString();
   }
}

Prosta metoda, przyjmująca typ generyczny. Dlaczego stanowi ona problem i wyzwanie dla twórców języka?

Jak wiemy, ToString to metoda zaimplementowana w klasie Object. Każdy zatem obiekt posiada ją. Wiemy również, że value types również dziedziczą po object (pośrednio). Nie ma zatem znaczenia czy mamy do czynienia z Integer czy z własną klasą – zawsze mamy do dyspozycji ToString. Oczywiście powoduje to problemy dla value type. Wywołując ToString powodujemy boxing, ponieważ należy skonwertować value type do Object, który jest typem referencyjnym.

Jak zatem zachowa się powyższa metoda? Na etapie kompilacji nie wiadomo czy ciało metody będzie operowało na value czy referencyjnym typie. Wiemy również, że ciało metody nie jest kopiowanie dla każdego typu generycznego a jest współdzielone – wyłącznie layout dla typów value jest różny, a sama logika (ciała metod) są zawsze współdzielone. Z tego względu, jak IL będzie wyglądać? Skąd CLR ma wiedzieć czy dokonać boxingu czy można ToString bezpośrednio wykonać? Zajrzyjmy do IL Assembly. Sama klasa będzie zaprezentowana następująco:

.class private auto ansi beforefieldinit ConsoleApplication16.CustomStack`1<T>
    extends [mscorlib]System.Object
{
    // Methods
    .method public hidebysig 
        instance void DoSomething (
            !T 'value'
        ) cil managed 
    {
        // Method begins at RVA 0x2094
        // Code size 16 (0x10)
        .maxstack 1
        .locals init (
            [0] string str
        )

        IL_0000: nop
        IL_0001: ldarga.s 'value'
        IL_0003: constrained. !T
        IL_0009: callvirt instance string [mscorlib]System.Object::ToString()
        IL_000e: stloc.0
        IL_000f: ret
    } // end of method CustomStack`1::DoSomething

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x20b0
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method CustomStack`1::.ctor

} // end of class ConsoleApplication16.CustomStack`1

Z IL widać jeszcze raz, że typy generyczne to nie sztuczka kompilatora, ale rozpoznawana konstrukcja w CLR. Przejdźmy teraz do metody, która nas najbardziej interesuje:

// Code size 16 (0x10)
.maxstack 1
.locals init (
    [0] string str
)

IL_0000: nop
IL_0001: ldarga.s 'value'
IL_0003: constrained. !T
IL_0009: callvirt instance string [mscorlib]System.Object::ToString()
IL_000e: stloc.0
IL_000f: ret

IL_0003 to contrained. !T.  Gdy callvirt wywołuje jakaś metodę, i wcześniej mamy contrained to zostanie boxing wykonany jeśli mamy do czynienia z value type. Innymi słowy, dla typów referencyjnych nic nie zmieni się (nie ma takiej potrzeby) a dla value najpierw wartość zostanie poddana boxingu.  Oczywiście mowa tylko o metodach wirtualnych. Jeśli wykonujemy metodę na typu value, która jest w nim zaimplementowana, wtedy nie ma takiej potrzeby.

Leave a Reply

Your email address will not be published.