Obsługa wyjątków w c# 6.0

Posted October 19th, 2014 by Piotr Zieliński
Categories: C#

Nowa wersja języka wprowadzi również ulepszenia w obsłudze wyjątków. Często tworzymy jeden wyjątek typu OperationFailedException, a w nim enum, który określa dlaczego operacja nie powiodła się tzn.:

public class OperationFailedException : Exception
{
   public OperationFailedException(int statusCode) { StatusCode = statusCode; }        

   public int StatusCode { get; set; }
}

Co jeśli chcemy napisać obsługę wyjątków, ale wyłącznie tych ze statusem 5? Dzisiaj możemy łapać wszystkie wyjątki i sprawdzać jaki status został zwrócony:

try
{
 throw new OperationFailedException(5);
}
catch(OperationFailedException e)
{
 if (e.StatusCode != 5)
     throw;

 // recovery
 Console.WriteLine("Recovery");
}

Czasami jest więcej warunków i sprawa po prostu komplikuje się. W C# 6.0 możliwe jest łapanie wyjątków za pomocą filtra tzn.:

 try
  {
      throw new OperationFailedException(5);
  }
  catch(OperationFailedException e) if(e.StatusCode==5)
  {       
      // recovery
      Console.WriteLine("Recovery");
  }

Wyłącznie OperationFailedException z StatusCode równym 5 będą wyłapywane. Oczywiście jak to ze wszystkimi ułatwieniami bywa, nie znaczy to, że powinniśmy tworzyć jeden wyjątek z wieloma statusami. Wciąż lepiej rozdzielać wyjątki na wiele klas. Czasami po prostu nie ma sensu tworzyć dwóch bardzo podobnych klas i lepiej zaimplementować to za pomocą jednej ale właśnie ze statusem.

Zajrzyjmy teraz do ILSpy:

private static void Main(string[] args)
{
    try
    {
        throw new OperationFailedException(5);
    }
    object arg_09_0;
    OperationFailedException expr_0E = arg_09_0 as OperationFailedException;
    int arg_22_0;
    if (expr_0E == null)
    {
        arg_22_0 = 0;
    }
    else
    {
        OperationFailedException e = expr_0E;
        arg_22_0 = (((e.StatusCode == 5) > false) ? 1 : 0);
    }
    endfilter(arg_22_0);
}

Tutaj widzimy pewną zmianę. Możemy zauważyć instrukcję endfilter. Tak naprawdę istniała ona od dawna w IL tylko C# nigdy jej nie wykorzystywał. Więcej informacji o instrukcji tutaj. W skrócie – to ona właśnie filtruje wyjątki na podstawie przekazanej flagi.

c# 6.0–inicjalizacja słowników oraz automatycznych właściwości

Posted October 16th, 2014 by Piotr Zieliński
Categories: C#

Najpierw zobaczymy, jak mogła wyglądać inicjalizacja słownika przed wersją 6.0:

var data = new Dictionary<string, int>()
{
 {"Klucz1",1 },
 {"Klucz2",114 },
 {"Klucz3",114 },
 {"Klucz5",41 }
};

Składnia trochę ciężka w czytaniu. W C# 6.0 możliwe będzie:

var data = new Dictionary<string, int>()
{
 ["Klucz1"] = 1,
 ["Klucz2"] = 114,
 ["Klucz3"] = 114,
 ["Klucz5"] = 41
};

Moim zdaniem dużo lepiej to wygląda. Drobna zmiana, a zwiększa czytelność kodu.

Kolejna natomiast zmiana nie podoba mi się i na szczęście została wycofana. Chciano wprowadzić nowy sposób uzyskiwania dostępu do pól. Klasycznie można odczytać wartość za pomocą:

var data = new Dictionary<string, int>()
{
    ["Klucz1"] = 1,
    ["Klucz2"] = 114,
    ["Klucz3"] = 114,
    ["Klucz5"] = 41
};

Console.WriteLine(data["Klucz1"]);

Alternatywą miało być:

var data = new Dictionary<string, int>()
  {
      $Klucz1 = 1,
      $Klucz2 = 114,
      $Klucz3 = 114,
      $Klucz4 = 41
  };

  Console.WriteLine(data.$Klucz1);

Z tego co wiem jednak, na szczęście nie będzie tego w c# 6.0

Kolejna nowość to inicjalizowanie automatycznych pól. Zawsze irytowało mnie, że samemu musiałem w konstruktorze przypisywać wartości tym właściwościom. Teraz możemy:

class Person
{
  public string FirstName { get; set; }="Piotr";
  public string LastName { get; set; }="Zielinski";
}    

Co prawda sama składnia jest trochę dziwna, ale do zaakceptowania. Nie wiem w sumie jakby to lepiej byłoby zaprojektować (może zwykły atrybut?). W każdym razie nie odstrasza mnie to tak bardzo jak planowany znak $ dla słowników.

Z powyższego wynika również, że można tworzyć właściwości tylko z getterem:

class Person
{
  public string FirstName { get; } ="Piotr";
  public string LastName { get; } = "Zielinski";
}    

Zaglądając do IL, przekonamy się, że nie jest to żadna zmiana w CLR tylko w czystym c# (co nie powinno dziwić):

.class nested private auto ansi beforefieldinit Person
    extends [mscorlib]System.Object
{
    // Fields
    .field private initonly string '<FirstName>k__BackingField'
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [mscorlib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggerBrowsableState) = (
        01 00 00 00 00 00 00 00
    )
    .field private initonly string '<LastName>k__BackingField'
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [mscorlib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggerBrowsableState) = (
        01 00 00 00 00 00 00 00
    )

    // Methods
    .method public hidebysig specialname 
        instance string get_FirstName () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x206c
        // Code size 11 (0xb)
        .maxstack 1
        .locals init (
            [0] string
        )

        IL_0000: ldarg.0
        IL_0001: ldfld string ConsoleApplication2.Program/Person::'<FirstName>k__BackingField'
        IL_0006: stloc.0
        IL_0007: br.s IL_0009

        IL_0009: ldloc.0
        IL_000a: ret
    } // end of method Person::get_FirstName

    .method public hidebysig specialname 
        instance string get_LastName () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x2084
        // Code size 11 (0xb)
        .maxstack 1
        .locals init (
            [0] string
        )

        IL_0000: ldarg.0
        IL_0001: ldfld string ConsoleApplication2.Program/Person::'<LastName>k__BackingField'
        IL_0006: stloc.0
        IL_0007: br.s IL_0009

        IL_0009: ldloc.0
        IL_000a: ret
    } // end of method Person::get_LastName

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

        IL_0000: ldarg.0
        IL_0001: ldstr "Piotr"
        IL_0006: stfld string ConsoleApplication2.Program/Person::'<FirstName>k__BackingField'
        IL_000b: ldarg.0
        IL_000c: ldstr "Zielinski"
        IL_0011: stfld string ConsoleApplication2.Program/Person::'<LastName>k__BackingField'
        IL_0016: ldarg.0
        IL_0017: call instance void [mscorlib]System.Object::.ctor()
        IL_001c: nop
        IL_001d: ret
    } // end of method Person::.ctor

    // Properties
    .property instance string FirstName()
    {
        .get instance string ConsoleApplication2.Program/Person::get_FirstName()
    }
    .property instance string LastName()
    {
        .get instance string ConsoleApplication2.Program/Person::get_LastName()
    }

} // end of class Person

C# 6.0–primary constructors

Posted October 13th, 2014 by Piotr Zieliński
Categories: C#

Update [16.10.2014]: Niestety opisana konstrukcja prawdopodobnie nie znajdzie się w finalnej wersji C# 6.0. Możliwe, że wcześniej czy później zostanie to zaimplementowane, ale wiadomo, różnie z tym bywa.

Czas zacząć pisać o nowym c#.  Najpierw pobierzmy wersję CTP z:

http://www.visualstudio.com/en-us/downloads/visual-studio-14-ctp-vs.aspx

Oczywiście to preview, więc odradzam instalowanie tego w pracy, chyba, że na VM. Lepiej nie ryzykować re-instalacją wszystkiego od nowa.

Często w kodzie można spotkać następujący wzorzec:

class Person
{
  private readonly string _firstName;
  private readonly string _lastName;

  public Person(string firstName,string lastName)
  {
      _firstName = firstName;
      _lastName = lastName;
  }
}

Kod często praktykowany, ale trochę nadmiarowy. Musimy zdefiniować konstruktor, tylko po to aby zainicjalizować kilka zmiennych. W C# do dyspozycji będziemy mieli następującą konstrukcję:

class Person(string firstName,string lastName)
{
  private readonly string _firstName=firstName;
  private readonly string _lastName = lastName;

  public override string ToString()
  {
      return string.Format("{0} {1}", _firstName, _lastName);
  }
}

Konstrukcja przydatna, aczkolwiek trochę dziwna. Nic takiego nie mieliśmy w CPP czy w Javie. Myślę jednak, że można przyzwyczaić się i jest to po prostu praktyczne.

Jest to najzwyklejszy konstruktor i Person zainicjalizujemy następująco:

Person person = new Person("piotr","zielinski");
Console.WriteLine(person.ToString());

Jeśli jednak będziemy chcieli skompilować to, dostaniemy błąd:

Error    1    Feature 'primary constructor' is only available in 'experimental' language version.    c:\ConsoleApplication2\ConsoleApplication2\Program.cs    17    21    ConsoleApplication2

Nic dziwnego. To tylko preview, nie ma pewności nawet czy ta konstrukcja będzie w oficjalnej wersji. Jeśli jednak chcemy to odpalić, przejdźmy do edycji pliku projektu i dodajmy tag LangVersion z ustawioną wartością “Experimental”:

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <LangVersion>Experimental</LangVersion>
  </PropertyGroup>

Próba użycia konstruktora bezparametrowego zakończy się również błędem:

Person person = new Person();

Tak jak wcześniej, zdefiniowanie niestandardowego konstruktora powoduje, że domyślny nie jest już automatycznie generowany.

Nic nie stoi na przeszkodzie, abyśmy pomieszali konstruktory:

class Person(string firstName,string lastName)
{
  private readonly string _firstName=firstName;
  private readonly string _lastName = lastName;
  private int _age;

  public Person(string firstName,string lastName,int age) :this(firstName,lastName)
  {
      _age = age;
  }
}

Oczywiście możemy  zdefiniować wyłącznie jeden primary constructor.

Zajrzyjmy również do IL:

.class nested private auto ansi beforefieldinit Person
    extends [mscorlib]System.Object
{
    // Fields
    .field private initonly string _firstName
    .field private initonly string _lastName
    .field private int32 _age

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            string firstName,
            string lastName
        ) cil managed 
    {
        // Method begins at RVA 0x2067
        // Code size 22 (0x16)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: stfld string ConsoleApplication2.Program/Person::_firstName
        IL_0007: ldarg.0
        IL_0008: ldarg.2
        IL_0009: stfld string ConsoleApplication2.Program/Person::_lastName
        IL_000e: ldarg.0
        IL_000f: call instance void [mscorlib]System.Object::.ctor()
        IL_0014: nop
        IL_0015: ret
    } // end of method Person::.ctor

    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            string firstName,
            string lastName,
            int32 age
        ) cil managed 
    {
        // Method begins at RVA 0x207e
        // Code size 18 (0x12)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: ldarg.2
        IL_0003: call instance void ConsoleApplication2.Program/Person::.ctor(string, string)
        IL_0008: nop
        IL_0009: nop
        IL_000a: ldarg.0
        IL_000b: ldarg.3
        IL_000c: stfld int32 ConsoleApplication2.Program/Person::_age
        IL_0011: ret
    } // end of method Person::.ctor

} // end of class Person

Tutaj nic nie zmieniło się. Konstruktor został po prostu wygenerowany w klasyczny sposób.

nServiceBus, część II–pierwszy przykład obsługi wiadomości

Posted October 10th, 2014 by Piotr Zieliński
Categories: Programowanie rozproszone

W poprzednim w poście było dość teoretycznie, ale o to mi chodziło. Nie chciałem tworzyć wszystkich wpisów wyłącznie o API. Dzisiaj z kolei będzie już prosty przykład, na którym później będziemy testować to co zostało opisane w pierwszym poście.

Zacznijmy od zdefiniowania wiadomości, którą chcemy wysyłać. Zwykle tworzy się osobny projekt dla nich. Stwórzmy więc projekt Contracts i zainstalujmy nServicebBus:

image

Nasza wiadomość będzie nazywać się AddPerson:

public class AddPerson:IMessage
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
}

Proszę zauważyć, że implementujemy IMessage. W praktyce korzysta się jeszcze z ICommand oraz IEvent, ale o tym kiedy indziej – w tym poście chce pokazać najprostszy przykład.

Powyższą klasę dodaliśmy do osobnego projektu Contracts, który będzie współdzielony zarówno przez serwer jak i klient.

Następnie możemy dodać projekt dla backend oraz zainstalować nServiceBus.Host:

image

Pakiet stworzy automatycznie klasę do konfiguracji hosta:

using NServiceBus;

/*
    This class configures this endpoint as a Server. More information about how to configure the NServiceBus host
    can be found here: http://particular.net/articles/the-nservicebus-host
*/
public class EndpointConfig : IConfigureThisEndpoint, AsA_Server
{
   public void Customize(BusConfiguration configuration)
   {
       configuration.UsePersistence<PLEASE_SELECT_ONE>();
   }
}

Jak widać musimy wybrać persistance model. Dla tego wpisu nie ma to większego znaczenia, ale będziemy posługiwać się bazą RavenDB. Oznacza to, że musimy zainstalować kolejny pakiet:

image

Konfiguracja zatem powinna wyglądać tak:

public class EndpointConfig : IConfigureThisEndpoint, AsA_Server
{
   public void Customize(BusConfiguration configuration)
   {
      configuration.UsePersistence<RavenDBPersistence>();
   }
}

Oczywiście sami musimy zainstalować sobie bazę RavenDB. Na razie proszę nie martwić się o to, co będzie tam przechowywane (o tym innym razem).

Jeśli będzie nam brakować jakiś komponentów warto zainstalować skrypty PowerShell (z konsoli NuGet):

PM> Install-Package NServiceBus.PowerShell
PM> Import-Module .\packages\NServiceBus.PowerShell.4.3.0\lib\net40\NServiceBus.PowerShell.dll

Na przykład, aby skonfigurować performance counter, wystarczy:

PM> Install-NServiceBusPerformanceCounters

Pozostało nam dodać handler, który obsłuży wiadomość AddPerson:

class AddPersonHandler:IHandleMessages<AddPerson>
{
   public void Handle(AddPerson message)
   {
       Console.WriteLine("Dodawanie osoby: {0} {1}",message.FirstName,message.LastName);
   }
}

Kolejnym projektem będzie klient. Dla uproszczenia może być to aplikacja konsolowa.

Najpierw musimy stworzyć szynę danych po której będziemy wysyłać komendy i zdarzenia:

var busConfiguration = new BusConfiguration();
busConfiguration.UsePersistence<RavenDBPersistence>();
ISendOnlyBus bus = Bus.CreateSendOnly(busConfiguration);

Wysłanie wiadomości wygląda następująco:

while (true)
{
 string firstName = Console.ReadLine();
 string lastName = Console.ReadLine();

 bus.Send(new AddPerson() {FirstName = firstName, LastName = lastName});
}

Powyższa próba wysłania wiadomości zostanie zakończona wyjątkiem:

Additional information: No destination could be found for message type Example.Contracts.AddPerson. Check the <MessageEndpointMappings> section of the configuration of this endpoint for an entry either for this specific message type or for its assembly.

Po prostu musimy zdefiniować, gdzie chcemy wysłać wiadomość. Domyślnie nazwa serwera to assembly name. Jeśli zatem, stworzyliśmy handler i serwer w Example.Backend to wystarczy:

bus.Send("Example.Backend@localhost",new AddPerson() {FirstName = firstName, LastName = lastName});

Oczywiście jest to mało eleganckie podejście. Lepiej jakbyśmy użyli pliku konfiguracyjnego. NuGet wygenerował już nam trochę wskazówek w config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="UnicastBusConfig" type="NServiceBus.Config.UnicastBusConfig, NServiceBus.Core" />
  </configSections>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>

  <UnicastBusConfig>
    <MessageEndpointMappings>
      <add Assembly="Example.Contracts" Endpoint="Example.Backend@localhost" />            
    </MessageEndpointMappings>
  </UnicastBusConfig>
</configuration>

Plik konfiguracyjny daje nam dużą swobodę. Możemy określić cały assembly, namespace, albo wyłącznie konkretny typ. Jak widać stanowi to po prostu mapowanie między typem wiadomości a endpoint’em. W przykładzie zaprezentowanym wszystkie wiadomości zdefiniowane w Example.Contracts będą wysyłane do Example.Backend.

Po uruchomieniu przykładu i wpisaniu imienia oraz nazwiska, wiadomość zostanie wysłana do serwera z uwzględnieniem systemu kolejek, o których pisałem parę dni temu.

W kolejnym wpisie przejrzymy dokładnie się, jak wiadomość wędruje między klientem a serwerem oraz co stanie się, gdy w międzyczasie padnie serwer.

Systemy oparte na kolejkach komunikatów na przykładzie nServiceBus, część I

Posted October 7th, 2014 by Piotr Zieliński
Categories: Patterns & Practices, Programowanie rozproszone

W kilku postach mam zamiar opisać bibliotekę nServiceBus. Nie chodzi mi jednak o opis samego API, a zastanowienie się, kiedy warto z takiej architektury skorzystać. Większość programistów wciąż projektuje systemy na zasadzie klient-serwer. W wielu przypadkach jest to wystarczające rozwiązanie. Nie zawsze musimy tworzyć skalowalne rozwiązania. NoSQL, hadoop, chmura mają zastosowanie ale w wielu przypadkach jest to po prostu niepotrzebne. Nie każdy tworzy oprogramowanie, wykorzystywane przez miliony użytkowników. Wracając jednak do tematu. W przypadku nServiceBus chciałbym skupić się bardziej na scenariuszach w których klasyczne przetwarzanie zapytań może po prostu doprowadzić do zawieszenia systemu.

Klasyczny model to RPC – remote procedure call. Polega on na wywołaniu metody (web service, kontroler w MVC) i zwróceniu natychmiast wyniku. Jest to bardzo proste i  analogiczne do wywoływania zwykłej metody in-memory. Kod można pisać sekwencyjnie czyli natychmiast po wywołaniu metody będziemy posiadać dostęp do tego co wróci – bez potrzebny implementacji callback’ow. Ta zaleta (łatwość obsługi) bezpośrednio stanowi największą wadę, a mianowicie brak skalowalności. Zastanówmy się co może przytrafić nam się w momencie wysłania zapytania przez klienta:

  1. Serwer tymczasowo będzie niedostępny.
  2. Sieć zostanie odłączona.
  3. Sieć będzie bardzo obciążona, stąd na rezultat przyjdzie czekać dłużej niż zwykle.

W przypadku RPC, gdy serwer nie działa, oczywiście zapytanie zakończy się błędem lub blokowaniem klienta. Co jeśli kilka tysięcy klientów wyśle zapytanie w tej samej chwili? Prymitywna implementacja RPC stworzy kilka tysięcy wątków, co oczywiście będzie miało katastrofalne skutki. RPC przede wszystkim nie jest odporne na awarie zarówno po stronie klienta jak i serwera. Możliwe scenariusze to:

  1. Klient wysyła zapytanie ale serwer w międzyczasie ulega awarii stąd nie jest w stanie obsłużyć zapytania.
  2. Klient wysyła zapytanie, serwer obsługuje je ale w miedzy czasie klient ulega awarii. Z tego względu nie będzie możliwe otrzymanie odpowiedzi.

Do tego dochodzi czas wykonania powyższych operacji. RPC przyjmuje, że jest on stały i nie powinien blokować użytkownika na zbyt duży czas.

Kolejny problem to skalowalność. RPC to prosta architektura klient-serwer. Nie ma możliwości dystrybucji zapytań między różne węzły. Oczywiście to uproszczenie bo możemy mieć w końcu load balancer i kilka serwerów obsługujących zapytania. Im bardziej jednak będziemy zapytania “rozpraszać”, tym szybciej dojdziemy do architektury odmiennej od RPC.

Klasyczny model RPC możemy przedstawić zatem następująco:

image

Prosta sprawa, wysyłamy zapytanie i dostajemy od razu odpowiedź. Jeśli wywołamy kolejno metody A i B to najpierw uzyskamy wynik A a potem B – kolejność zachowana.

Przejdźmy teraz do wyjaśnienia jak działa nServiceBus i podobne technologie. Przede wszystkim mamy do dyspozycji kolejki. Serwer zamiast próbować obsłużyć wszystkie zadania jednocześnie, przechowuje dane na kolejce. W przypadku nServiceBus jest to MSMQ (Microsoft’owa implementacja). MSMQ to jest temat sam w sobie na długi artykuł albo książkę więc nie będę tutaj wyjaśniał API. Każdy węzeł (klient, serwer) posiada dwie grupy kolejek:

  1. kolejki wiadomości wychodzących
  2. kolejki wiadomości przychodzących

Dobrym porównaniem systemów opartych o komunikaty (wiadomości) jest poczta email. Jeśli osoba A wysyła email do  osoby B, to odbierze ona go, gdy będzie miała czas. Osoba A nie spodziewa się natychmiastowej odpowiedzi. Wysyła po prostu swoje pytanie (wiadomość). Następnie osoba B w wolnym czasie odbiera wiadomość, czyta i odpowiada. Analogicznie osoba A odczyta odpowiedź, gdy będzie miała czas.

Powróćmy znów do nServiceBus. Jeśli klient wysyła wiadomość do serwera będzie wyglądać to następująco:

image

Najpierw klient umieszcza wiadomość na wewnętrznej kolejce wiadomości wychodzących. Nawet jak połączenie między klientem a serwerem zostanie zerwane, wiadomość będzie przechowywana w kolejce i gotowa do wysłania, gdy tylko połączenie znów będzie działać. Następnie, po wysłaniu, zostanie ona umieszczona w kolejce wiadomości przychodzących drugiego węzła. Jeśli serwer jest bardzo zajęty, zostanie ona po prostu tam przechowywana aż do momentu, kiedy może zostać obsłużona. W pewnym momencie serwer zdejmie wiadomość, wykona dane zadanie i wygeneruje odpowiedź. Odpowiedz zwracana jest również w analogiczny sposób:

image

Serwer po wygenerowaniu odpowiedzi umieszcza ją w kolejce wiadomości wychodzących. Następnie, jak tylko będzie taka możliwość, jest ona przesyłana do klienta i umieszczana w kolejce komunikatów przychodzących. Klient potem zdejmuje ją z kolejki i obsługuje odpowiedź w odpowiedni sposób.

Dzięki dwóm typom kolejek, jesteśmy odporni na awarie sieci. Zmienia natomiast to sposób jak komunikujemy się z serwerem. Nie jest to model zapytanie-odpowiedź! Należy mieć to na uwadze, że nie ma gwarancji kiedy dostaniemy odpowiedź. Nie zawsze taki mechanizm nadaje się w danej aplikacji. Pojęcia klient oraz serwer również nie są prawidłowe i będę dalej posługiwać się słowem “węzeł”. Bardzo często w systematach opartych o kolejki mamy np. 5 węzłów i każdy z nich może przetwarzać te same zapytanie.

To dopiero początek. W przyszłym wpisie pokażę jakiś prosty system oparty o nServiceBus.  Więcej czasu w kolejnych wpisach również chcę przeznaczyć, jak nServiceBus dba o to, aby wiadomość dotarła i została przetworzona. Póki, co wiemy, że w przypadku awarii sieci, wiadomość pozostaje w kolejce. Co jednak w przypadku, gdy obsługa wiadomości zakończyła się wyjątkiem?

Implementacja wewnętrzna: dynamic

Posted October 4th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

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

Emisja IL: DynamicMethod

Posted September 25th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

W przyszłym poście zajmę się słowem kluczowym dynamic od strony IL. Najpierw jednak chciałbym zaprezentować klasę DynamicMethod, ułatwiającą tworzenie własnych metod w oparciu o emisję IL. Jest ona bardzo prosta w użyciu i bez żadnej, skomplikowanej konfiguracji wystarczy po prostu wygenerować IL (co jest oczywiście trudnym zadaniem). Przykład:

MethodInfo writeLineMethod = typeof(Console).GetMethod("WriteLine", types: new Type[] { typeof(string) });

var dynamicMethod = new DynamicMethod("SayHello", typeof (string), new Type[] {}, typeof (Program));

ILGenerator generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Ldstr, "Hello World !");
generator.EmitCall(OpCodes.Call, writeLineMethod, null);
generator.Emit(OpCodes.Ldstr, "Hello World 2 !");
generator.Emit(OpCodes.Ret);

var sayHelloMethod =(Func<string>)dynamicMethod.CreateDelegate(typeof(Func<string>));

string result = sayHelloMethod();

Wyemitowany IL wyświetli i zwróci tekst. Za pomocą Ldstr ładujemy na stos pierwszy napis, potem wykonujemy metodę Call, która pobierze ze stosu wspomniany napis. Na końcu zwracamy kolejny napis. Język IL opisywałem w kilkunastu postach więc jeśli instrukcje nie są jasne to zachęcam do poszperania na blogu.

Kolejne pytanie, po co nam to? Na pewno przyda się w analizowaniu internali i płynących z tego czasami optymalizacji. Jedną z tych optymalizacji jest zastąpienie mechanizmu refleksji dynamic method. Powyższy kod to nic innego jak wywołanie metody, do której nie mamy bezpośrednio dostępu.

Uproszczając przykład możemy:

MethodInfo toUpper = typeof (string).GetMethod("ToUpper", new Type[0]);            
var dynamicMethod = new DynamicMethod("ToUpperWrapper", typeof (string), new Type[] {typeof (string)});

ILGenerator il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, toUpper);
il.Emit(OpCodes.Ret);

var toUpperWrapper = (Func<string, string>) dynamicMethod.CreateDelegate(typeof (Func<string, string>));

string result = toUpperWrapper("Hello world");

Kod pokazuje jak wywołać metodę ToUpper tak jakbyśmy korzystali z reflection. Z tym, że jest to o WIELE szybsze i jeśli tylko musimy korzystać w aplikacji z reflection, lepiej to zrobić za pomocą DynamicMethod albo dynamic.

Napiszmy prosty benchmark:

class Program
{
   private const int N = 1000000000;
   private static void TestReflectionInvoke()
   {
       MethodInfo toUpper = typeof(string).GetMethod("ToUpper", new Type[0]);

       var stopwatch = Stopwatch.StartNew();
       object test = "Hello World";

       for (int i = 0; i < N; i++)
           test = toUpper.Invoke(test, null);

       stopwatch.Stop();
       Console.WriteLine("Reflection:{0}", stopwatch.ElapsedTicks);
   }
   private static void TestDynamicMethod()
   {                       
       MethodInfo toUpper = typeof (string).GetMethod("ToUpper", new Type[0]);
       
       var stopwatch = Stopwatch.StartNew();
       var dynamicMethod = new DynamicMethod("ToUpperWrapper", typeof (string), new Type[] {typeof (string)});

       ILGenerator il = dynamicMethod.GetILGenerator();
       il.Emit(OpCodes.Ldarg_0);
       il.Emit(OpCodes.Call, toUpper);
       il.Emit(OpCodes.Ret);
       
       var toUpperWrapper = (Func<string, string>) dynamicMethod.CreateDelegate(typeof (Func<string, string>));

       object test = "Hello World";

       for (int i = 0; i < N; i++)
           test = toUpperWrapper("Hello world");
   
       Console.WriteLine("Dynamic method:{0}", stopwatch.ElapsedTicks);
   }

   static void Main(string[] args)
   {
       TestDynamicMethod();
       TestReflectionInvoke();
   }

}

Różnica jest znacząca, na korzyść DynamicMethod:

image

Dobre praktyki – wydajność async\await dla skomplikowanych wyrażeń

Posted September 22nd, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices, Wielowątkowość

O dobrych praktykach async\await pisałem już wielokrotnie. Dzisiaj zacznijmy od code review:

private static async void Test()
{
  int result = SyncFunction1() + SyncFunction2()*await TestAsync() + SyncFunction3();
}

private static int SyncFunction1()
{
  // jakas logika
  return 1;
}
private static int SyncFunction2()
{
  // jakas logika
  return 1;
}

private static int SyncFunction3()
{
  // jakas logika
  return 1;
}
private static async Task<int> TestAsync()
{
  return 5;
}

W powyższym kodzie mamy skomplikowane wyrażenie, które wygląda na synchroniczne:

int result = SyncFunction1() + SyncFunction2()*await TestAsync() + SyncFunction3();

Wiemy, że tak naprawdę musi zostać wygenerowana maszyna stanów. Powyższy przykład zawiera również wyrażenie, które jest przetwarzane standardowo od lewej do prawej strony, z uwzględnieniem nawiasów i kolejności wykonywania operatorów. Gdyby nie kod asynchroniczny, byłoby to łatwe – po prostu na stosie kolejne wyniki byłyby przechowywane.

Standardowy stos w async nie może zostać użyty bo po wskoczeniu w metodę asynchroniczną całość zostałaby wyczyszczona.

Z tego względu w .NET zaimplementowano tzw. stack spilling. Polega to na przeniesieniu wartości, które normalnie znajdowałby się na stosie, na stertę (stack->heap). Ma to oczywiście pewne konsekwencje takie jak dodatkowa alokacja obiektów czy boxing. Każdy zaalokowany obiekt referencyjny posiada kilka dodatkowych pól w przeciwieństwie do bardziej oszczędnych struktur. Ponadto GC będzie miał dodatkową robotę i powoduje to najwięcej strat. Przyjrzyjmy się wygenerowanej maszynie stanów:

[CompilerGenerated]
[StructLayout(LayoutKind.Auto)]
private struct <Test>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncVoidMethodBuilder <>t__builder;
public int <result>5__1;
private TaskAwaiter<int> <>u__$awaiter2;
private object <>t__stack;
void IAsyncStateMachine.MoveNext()
{
    try
    {
        int num = this.<>1__state;
        if (num != -3)
        {
            int arg_BF_0;
            int arg_BE_0;
            TaskAwaiter<int> taskAwaiter;
            if (num != 0)
            {
                int expr_1E = arg_BF_0 = Program.SyncFunction1();
                int expr_23 = arg_BE_0 = Program.SyncFunction2();
                taskAwaiter = Program.TestAsync().GetAwaiter();
                if (!taskAwaiter.IsCompleted)
                {
                    Tuple<int, int> tuple = new Tuple<int, int>(expr_1E, expr_23);
                    this.<>t__stack = tuple;
                    this.<>1__state = 0;
                    this.<>u__$awaiter2 = taskAwaiter;
                    this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<Test>d__0>(ref taskAwaiter, ref this);
                    return;
                }
            }
            else
            {
                Tuple<int, int> tuple = (Tuple<int, int>)this.<>t__stack;
                arg_BF_0 = tuple.Item1;
                arg_BE_0 = tuple.Item2;
                this.<>t__stack = null;
                taskAwaiter = this.<>u__$awaiter2;
                this.<>u__$awaiter2 = default(TaskAwaiter<int>);
                this.<>1__state = -1;
            }
            int arg_BE_1 = taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter<int>);
            int num2 = arg_BF_0 + arg_BE_0 * arg_BE_1 + Program.SyncFunction3();
            this.<result>5__1 = num2;
        }
    }
    catch (Exception exception)
    {
        this.<>1__state = -2;
        this.<>t__builder.SetException(exception);
        return;
    }
    this.<>1__state = -2;
    this.<>t__builder.SetResult();
}
[DebuggerHidden]
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
{
    this.<>t__builder.SetStateMachine(param0);
}
}

Na początku widzimy deklaracje pól wewnętrznych maszyny:

public int <>1__state;
public AsyncVoidMethodBuilder <>t__builder;
public int <result>5__1;
private TaskAwaiter<int> <>u__$awaiter2;
private object <>t__stack;

Wszystkie zmienne (numer stanu, buiilder, awaiter) były już wcześniej omawiane na blogu. Nowością jest _stack czyli wspomniany stos przechowywany na stercie (proszę zauważyć, że jest to zawsze typ referencyjny).

W programowaniu asynchronicznym, jeśli potrzebujemy użyć stosu (evaluation stack) umieszczamy go właśnie w tym polu (__stack). W większości wypadków będzie to lista wartości i wtedy do __stack przypisuje się Tuple<…> ze wszystkimi częściowymi wynikami.

W pierwszym stanie wykonujemy wszystko co jest po lewej stronie await, czyli SyncFunction1 oraz SyncFunction2:

int expr_1E = arg_BF_0 = Program.SyncFunction1();
int expr_23 = arg_BE_0 = Program.SyncFunction2();
taskAwaiter = Program.TestAsync().GetAwaiter();
if (!taskAwaiter.IsCompleted)
{
    Tuple<int, int> tuple = new Tuple<int, int>(expr_1E, expr_23);
    this.<>t__stack = tuple;
    this.<>1__state = 0;
    this.<>u__$awaiter2 = taskAwaiter;
    this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<Test>d__0>(ref taskAwaiter, ref this);
    return;
}

Wynik na stosie jest przechowywany jako Tuple<int,int>:

Tuple<int, int> tuple = new Tuple<int, int>(expr_1E, expr_23);
this.<>t__stack = tuple;

Jest to pierwsza “zbędna” alokacja. W kolejnym stanie zdejmujemy wartości ze stosu, uzyskując wynik SyncFunction1 oraz SyncFunction2. Na tym etapie mamy   również wynik asynchronicznej funkcji TestAsync:

Tuple<int, int> tuple = (Tuple<int, int>)this.<>t__stack;
    arg_BF_0 = tuple.Item1;
    arg_BE_0 = tuple.Item2;
    this.<>t__stack = null;
    taskAwaiter = this.<>u__$awaiter2;
    this.<>u__$awaiter2 = default(TaskAwaiter<int>);
    this.<>1__state = -1;
}
int arg_BE_1 = taskAwaiter.GetResult();
taskAwaiter = default(TaskAwaiter<int>);
int num2 = arg_BF_0 + arg_BE_0 * arg_BE_1 + Program.SyncFunction3();
this.<result>5__1 = num2;

Przyjrzyjmy się teraz sytuacji, gdzie niezbędne jest posiadanie więcej niż tylko dwóch stanów:

int result = await TestAsync()*SyncFunction1() + SyncFunction2()*(await TestAsync() + SyncFunction3()*await TestAsync());

Jeśli zajrzymy do IlSpy, zobaczymy masę alokacji m.in:

Tuple<int, int> tuple = new Tuple<int, int>(expr_AA, expr_AB);

Tuple<int, int, int, int> tuple2 = new Tuple<int, int, int, int>(arg_162_0, arg_162_1, expr_139, expr_146);

“Prosty” przykład, może poskutkować wieloma alokacji. Z tego względu, czasami lepiej wywołania asynchroniczne uszeregować poza wyrażeniem tzn.:

int resultAsync = await TestAsync();
int result = SyncFunction1() + SyncFunction2()*resultAsync;

Jeśli możemy kilka metod asynchronicznych wykonać w tym samym czasie, wtedy dużo lepiej jest użyć Task.WhenAll.

Powyższy kod nie doprowadzi do stack spilling, a wyniki tymczasowe będą przechowywane w zwyczajnych zmiennych lokalnych. Skutek będzie taki, że maszyna stanów będzie posiadała wiele pól, ale jest to lepsze w wielu przypadkach niż wspomniane alokacje (czas + overhead). Z drugiej strony, pola w maszynie stanów będą w pamięci aż do jej zakończenia, co może być czasochłonne (programowanie asynchroniczne). Wartości ze sterty zawsze można zwolnić, np. podczas przechodzenia z jednego stanu w drugi.

IL Assembly: wywoływanie metod wirtualnych na typach generycznych

Posted September 19th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

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.

Typy generyczne w .NET–implementacja wewnętrzna

Posted September 16th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

Typy generyczne w .NET to nie żadna imitacja, a prawdziwa implementacja (w przeciwieństwie do Java), zoptymalizowana pod wieloma kątami.

Typy referencyjne są zaimplementowane w inny sposób niż value types. Załóżmy, że mamy następujący kod w c#:

class Program
{
   static void Main(string[] args)
   {
       var s1 = new CustomStack<int>();
       var s2 = new CustomStack<double>();
   }
}

class CustomStack<T>
{
   public T Value { get; set; }
   public object OtherProperty { get; set; }
   
   public string AnotherProperty { get; set; }
}

W przypadku value type zostaną wygenerowane dwie różne specjalizacje dla CustomStack. Każda próba stworzenia instancji CustomStack z innym typem, spowoduje utworzenie nowej specjalizacji. Sprawa wygląda jednak inaczej dla typów referencyjnych. Rozszerzmy przykład o:

var s1 = new CustomStack<int>();
var s2 = new CustomStack<double>();

var s3 = new CustomStack<Person>();
var s4 = new CustomStack<Customer>();

Specjalizacje dla Person i Customer będą dzielić ze sobą layout. Nie ważne jak wiele stworzymy specjalizacji, zawsze będą współdzielić ten sam layout. W .NET nie grozi nam eksplozja typów, która miałaby miejsce, gdy nie powyższa optymalizacja.

Sprawdźmy jednak to sami za pomocą debuggera windbg.exe. Zaczynamy standardowo od załadowania SOS:

.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dll

Następnie przyjrzyjmy się aktualnie załadowanym typom za pomocą:

!dumpheap -stat

Wynik:

image

Te co nas interesują  to:

003f3d08        1           20 ConsoleApplication16.CustomStack`1[[ConsoleApplication16.Customer, ConsoleApplication16]]
003f3c4c        1           20 ConsoleApplication16.CustomStack`1[[ConsoleApplication16.Person, ConsoleApplication16]]
003f399c        1           20 ConsoleApplication16.CustomStack`1[[System.Int32, mscorlib]]
003f3a80        1           24 ConsoleApplication16.CustomStack`1[[System.Double, mscorlib]]

Najpierw udowodnimy, że typy value nie współdzielą layoutu. Za pomocą powyższych adresów możemy wylistować method table używając następujących poleceń:

!dumpmt 003f3a80        
!dumpmt 003f399c               

Wynik:

// specjalizacja dla int
0:000> !dumpmt 003f399c        
EEClass:         003f1580
Module:          003f2ed4
Name:            ConsoleApplication16.CustomStack`1[[System.Int32, mscorlib]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
BaseSize:        0x14
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0

// specjalizacja dla double
0:000> !dumpmt 003f3a80        
EEClass:         003f15e0
Module:          003f2ed4
Name:            ConsoleApplication16.CustomStack`1[[System.Double, mscorlib]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0

To co rzuca się w oczy to adres EEClass – jest inny dla int niż dla double. W przyszłości chcę napisać post o internalach typów, ale na razie wystarczy wiedzieć, że EEClass opisuje listę metod (layout) dla danego typu. Przyjrzyjmy się zatem EEClass dla specjalizacji double.

Polecenie:

!dumpclass 003f15e0

Wynik:

Class Name:      ConsoleApplication16.CustomStack`1[[System.Double, mscorlib]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
Parent Class:    73713f70
Module:          003f2ed4
Method Table:    003f3a80
Vtable Slots:    4
Total Method Slots:  b
Class Attributes:    100000  
Transparency:        Critical
NumInstanceFields:   3
NumStaticFields:     0
      MT    Field   Offset                 Type VT     Attr    Value Name
73b1b454  4000001        4        System.Double  1 instance           <Value>k__BackingField
73b22554  4000002        c        System.Object  0 instance           <OtherProperty>k__BackingField
73b221b4  4000003       10        System.String  0 instance           <AnotherProperty>k__BackingField

Wyraźnie widzimy, że layout zawiera pole double:

73b1b454  4000001        4        System.Double  1 instance           <Value>k__BackingField

EEClass dla specjalizacji integer, będzie zawierało analogiczny layout ale z innym typem dla Value:

      MT    Field   Offset                 Type VT     Attr    Value Name
73b23b04  4000001        c         System.Int32  1 instance           <Value>k__BackingField
73b22554  4000002        4        System.Object  0 instance           <OtherProperty>k__BackingField
73b221b4  4000003        8        System.String  0 instance           <AnotherProperty>k__BackingField

Powróćmy teraz do typów referencyjnych. Analogicznie za pomocą !dumpmt wyświetlamy method table dla nich:

// specjalizacja Customer
0:000> !dumpmt 003f3d08        
EEClass:         003f1694
Module:          003f2ed4
Name:            ConsoleApplication16.CustomStack`1[[ConsoleApplication16.Customer, ConsoleApplication16]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
BaseSize:        0x14
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0
// specjalizacja Person
0:000> !dumpmt 003f3c4c        
EEClass:         003f1694
Module:          003f2ed4
Name:            ConsoleApplication16.CustomStack`1[[ConsoleApplication16.Person, ConsoleApplication16]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
BaseSize:        0x14
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0

Od razu widać, że EEClass jest taki sam i znajduje się pod adresem 003f1694. Poleceniem !dumpclass 003f1694 wyświetlamy jej zawartość:

      MT    Field   Offset                 Type VT     Attr    Value Name
73b24d7c  4000001        4       System.__Canon  0 instance           <Value>k__BackingField
73b22554  4000002        8        System.Object  0 instance           <OtherProperty>k__BackingField
73b221b4  4000003        c        System.String  0 instance           <AnotherProperty>k__BackingField

Co widzimy? Przede wszystkim System.__Canon jako typ dla Value. Innymi słowy, dla typów referencyjnych jest używany ten sam layout i zamiast konkretnego typu mamy System.__Canon.

To jeszcze nie koniec- w kolejnym wpisie zobaczymy kilka ciekawostek z wywoływania metod w generics…