Implementacja wewnętrzna: dynamic

Słowo dynamic jest często nadużywane prowadząc do trudno czytelnego kodu. Innym problemem jest wydajność – programiści często nie wiedzą jaki overhead za sobą ponosi każde użycie dynamic.  Zacznijmy eksperymentowanie z IL:

static void Main(string[] args)
{
  dynamic text = "Hello world";
}

Wygenerowany IL:

// Method begins at RVA 0x2050
// Code size 8 (0x8)
.maxstack 1
.entrypoint
.locals init (
[0] object text
)

IL_0000: nop
IL_0001: ldstr "Hello world"
IL_0006: stloc.0
IL_0007: ret

 

Nic nadzwyczajnego póki co – zmienna została zaprezentowana jako po prostu object. Kolejny krok:

dynamic text = "Hello world";
string textStr = text;
int textInt = text;

Dla uproszczenia pokażemy zdekompilowany c# zamiast IL:

// ConsoleApplication16.Program
private static void Main(string[] args)
{
    object text = "Hello world";
    if (Program.<Main>o__SiteContainer0.<>p__Site1 == null)
    {
        Program.<Main>o__SiteContainer0.<>p__Site1 = CallSite<Func<CallSite, object, string>>.Create(Binder.Convert(CSharpBinderFlags.None, typeof(string), typeof(Program)));
    }
    string textStr = Program.<Main>o__SiteContainer0.<>p__Site1.Target(Program.<Main>o__SiteContainer0.<>p__Site1, text);
    if (Program.<Main>o__SiteContainer0.<>p__Site2 == null)
    {
        Program.<Main>o__SiteContainer0.<>p__Site2 = CallSite<Func<CallSite, object, int>>.Create(Binder.Convert(CSharpBinderFlags.None, typeof(int), typeof(Program)));
    }
    int textInt = Program.<Main>o__SiteContainer0.<>p__Site2.Target(Program.<Main>o__SiteContainer0.<>p__Site2, text);
}

Widzimy, że mamy coś nowego tutaj – SiteContainer. Pierwsze dwa  IFy to lazy-loading – tworzymy tylko raz SiteContainer, który znajduje się jako statyczne pole.

W tym przypadku nie ma to wielkiego sensu ponieważ i tak metoda zostanie wykonana tylko raz – w końcu to main. Przedstawiony wzorzec leniwego ładowania jest zawsze wykorzystywany z dynamic – tworzony site container jest inicjalizowany wyłącznie raz.

Co site container zawiera? Przekazujemy mu informacje statyczne, wynikające z kontekstu. Wiemy, ze próbujemy przypisać przypisać najpierw dynamic do string a potem do int – stąd dwa kontenery. Każdy z nich zawiera osobny kontekst. Innymi słowy binder wraz z callsite zawierają informacje potrzebne do wykonania danej operacji – w tym przypadku przypisania jednej zmiennej do drugiej.

Samo wywołanie metody (tutaj przypisania) następuje poprzez Target:

string textStr = Program.<Main>o__SiteContainer0.<>p__Site1.Target(Program.<Main>o__SiteContainer0.<>p__Site1, text);

Jak wygląda implementacja Target? W przypadku przypisania mogłoby wyglądać to tak:

if (text == null || typeof(int).IsAssignableFrom(((object)text).GetType()) == false)
    throw new Microsoft.CSharp.RuntimeBinder.RuntimeBinderException("Cannot implicitly convert type 'string' to 'int'.");
int textInt = (int)text;

Dla każdego typu generowana jest nowa metoda. Jeśli mamy 100 różnych operacji na dynamic, zostanie wygenerowanych 100 różnych metod takich jak powyższe. Zobaczymy co się stanie jak będziemy próbować wykonać jakąś metodę na dynamic:

dynamic text = "Hello world";
string str = text.ToUpper();

// ConsoleApplication16.Program
private static void Main(string[] args)
{
    object text = "Hello world";
    if (Program.<Main>o__SiteContainer0.<>p__Site1 == null)
    {
        Program.<Main>o__SiteContainer0.<>p__Site1 = CallSite<Func<CallSite, object, string>>.Create(Binder.Convert(CSharpBinderFlags.None, typeof(string), typeof(Program)));
    }
    Func<CallSite, object, string> arg_93_0 = Program.<Main>o__SiteContainer0.<>p__Site1.Target;
    CallSite arg_93_1 = Program.<Main>o__SiteContainer0.<>p__Site1;
    if (Program.<Main>o__SiteContainer0.<>p__Site2 == null)
    {
        Program.<Main>o__SiteContainer0.<>p__Site2 = CallSite<Func<CallSite, object, object>>.Create(Binder.InvokeMember(CSharpBinderFlags.None, "ToUpper", null, typeof(Program), new CSharpArgumentInfo[]
        {
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
        }));
    }
    string str = arg_93_0(arg_93_1, Program.<Main>o__SiteContainer0.<>p__Site2.Target(Program.<Main>o__SiteContainer0.<>p__Site2, text));
}

Zostały wygenerowane dwa kontenery. Pierwszy z nich dla wykonania ToUpper a druga przypisania wyniku do textStr. Wykonanie ToUpper wygląda następująco:

Program.<Main>o__SiteContainer0.<>p__Site2 = CallSite<Func<CallSite, object, object>>.Create(Binder.InvokeMember(CSharpBinderFlags.None, "ToUpper", null, typeof(Program), new CSharpArgumentInfo[]
    {
        CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
    }));

Kod IL nie jest generowany za każdym razem, gdy w kodzie wykonujemy Target. Implementacja wewnętrzna posiada mechanizm buforowania i jeśli raz wykonamy ToUpper to kolejne wykonania będą już korzystały zbuforowanej metody testującej.

Podsumowując: tworząc callsite przekazujemy statyczne informacje takie jak: nazwa wykonywanej metody, typ generyczny (jeśli istnieje), zwracany typ, parametry wejściowe (wraz ref i out jeśli istnieją). Następnie wywołując Target, następuje wykonanie danej metody na podstawie informacji przekazanych statycznie, jak i tych uzyskanych już w trakcie działania aplikacji (dzięki automatycznie wygenerowanemu IL).