C# 5.0: Atrybuty CallerMemberName, CallerFilePath oraz implementacja interfejsu INotifyPropertyChanged

W c# 5.0 dodano dwa nowe atrybuty: CallerMemberName oraz CallerFilePath. Można je stosować do domyślnych parametrów metod:

static private void Print([CallerMemberName]string methodName = null, [CallerFilePath]string fileName = null)
{
  Console.WriteLine(methodName);
  Console.WriteLine(fileName);
}

Wywołanie powyższej metody bez podania argumentów spowoduje przekazaniem nazwy metody, która wywołała Print oraz ścieżki pliku w którym znajduje się ta metoda:

Print();

Jeśli Print został wywołany w metodzie Main wtedy methodName zawiera tekst “Main”.

Jak to jednak może się przydać w praktyce? Pierwsza sprawa to loggery – w łatwy sposób można zapisać nazwę pliku oraz stacktrace. Dla programistów WPF oraz ViewModel jednak warto pokazać pewny przykład, który ułatwia implementacje INotifyPropertyChanged. Jedną z możliwych implementacji w “dawnych” czasach była:

public class ViewModel : INotifyPropertyChanged
{
   private string _text;
   public string Text
   {
       get { return _text; }
       set
       {
           _text = value;
           OnPropertyChanged("Text");
         
       }
   }
   virtual protected void OnPropertyChanged(string propName)
   {
       if (PropertyChanged != null)
       {
           PropertyChanged(this, new PropertyChangedEventArgs(propName));
       }
   }
   public event PropertyChangedEventHandler PropertyChanged;
}

Czasami programiści zamiast przekazywać po prostu string wykorzystywali lambda. W C# 5.0 można jednak wykorzystać wspominany atrybut:

public class ViewModel : INotifyPropertyChanged
{
   private string _text;
   public string Text
   {
       get { return _text; }
       set
       {
           _text = value;
           RaisePropertyChanged();
         
       }
   }
   virtual protected void RaisePropertyChanged([CallerMemberName]string propName=null)
   {
       if (PropertyChanged != null)
       {
           PropertyChanged(this, new PropertyChangedEventArgs(propName));
       }
   }
   public event PropertyChangedEventHandler PropertyChanged;
}

Atrybut spowoduje przekazane do propName tekstu “Text”. Moim zdaniem łatwe i eleganckie – nie trzeba dłużej na sztywno przekazywać stringów albo korzystać z nieco wolniejszej lambdy.

Code review: bezpieczny dostęp do słownika

Często widzę następujący kod:

private  Dictionary<string,string> _dictionary=new Dictionary<string, string>();
//......
string value = _dictionary["Key"];

Oczywiście jeśli mamy pewność, że zawsze jest klucz nie ma z tym problemu. Czasami jednak takiej pewności nie ma i należy zwrócić NULL albo stworzyć dany element w słowniku. Wtedy zaczynają się problemy bo najczęściej jest to dokonywane za pomocą:

if (_dictionary.ContainsKey("Key"))
    return _dictionary["Key"];
//else
return null;

Powyższy kod będzie musiał dwa razy dokonać przeszukiwania słownika. Raz aby wykonać ContainsKey a potem drugi raz aby zwrócić element. Łączna złożoność to O(2). Jest to czas optymistyczny ponieważ gdy występują kolizje wtedy proces się wydłuża.  Szybszym i lepszym rozwiązaniem jest użyte metody TryGetValue:

string value;
_dictionary.TryGetValue("Key", out value);

Złożoność wynosi O(1). TryGetValue sprawdza czy klucz istnieje, jeśli tak to zwraca jego wartość w przeciwnym wypadku value wynosi NULL. W przeciwieństwie do zwykłego dostępu nie jest wyrzucany wyjątek. Jeśli zatem w pętli (np. w pętli renderującej) musimy dokonywać częstych operacji na słowniku to różnica między O(1) a O(2) ma już znaczenie bo w końcu może to być O(5000) a O(10000) – dwukrotnie szybsze rozwiązanie.

Statyczne metody – wydajność

Pytanie brzmi: czy metody statyczne są wydajniejsze niż ich “zwykłe” odpowiedniki (instance methods)? Odpowiedź brzmi tak, ponieważ:

  1. Wywołanie jakiejkolwiek niestatycznej metody na klasie powoduje przekazanie parametru “this” – a to wymaga dodatkowego czasu.
  2. Same wywołanie jest bardziej czasochłonne dla zwykłych metod. W klasach statycznych nie ma dziedziczenia i wirtualnych metod. W niestatycznych metodach, kompilator musi dokonać kilka  dodatkowych operacji. Każda metoda niestatyczna jest wywoływana za pomocą instrukcji IL callvirt a statyczna call. callvirt musi sprawdzić całą hierarchie klas, co jest więc z reguły bardziej czasochłonne.

Napiszmy więc przykładowy test:

Stopwatch stopwatch=new Stopwatch();
stopwatch.Restart();

stopwatch.Stop();
stopwatch.Restart();

// wywolanie statycznej metody
for (int i = 0; i < testsNumber; i++)
 A.Method();

stopwatch.Stop();

long staticTime = stopwatch.ElapsedTicks;

stopwatch.Restart();

// wywolanie niestatycznej metody
for (int i = 0; i < testsNumber; i++)
 b.Method();

stopwatch.Stop();

long instanceTime = stopwatch.ElapsedTicks;

Wynik:

staticTime    29864
instanceTime    54746

Jaki z tego wniosek? Osobiście nie martwię się wydajnością a skupiam się wyłącznie na design. Wybierając typ metody zastanawiam się bardziej co jest lepsze dla przejrzystości API. Dla biznesowych lub industrialnych aplikacji nie ma to znaczenia. Jeśli dla danego projektu taka mała różnica jest ważna to lepiej zastanowić się nad zmianą języka ponieważ c# nie jest stworzony dla takich problemów. Poza tym,  z moich obserwacji wynika kilka optymalizacji:

  1. Pierwsze wywołanie metody niestatycznej jest dużo bardziej czasochłonne niż pierwsze wywołanie metody statycznej.
  2. W trybie release wartości są dużą mniejsze – optymalizacja jest znacząca.

Myślę, że post należy traktować jako ciekawostkę i ewentualną wskazówkę do korzystania ze statycznych metod ale tylko wtedy gdy nie narusza to czytelności klasy.

Singleton a klasy statyczne

O wzorcu projektowym singleton napisałem już kiedyś sporawy post. Klasy statyczne są prostym mechanizmem i mogą przypominać singleton. Tak jak w singleton, w klasach statycznych mamy do dyspozycji wyłącznie jedną instancję obiektu. Również nie ma możliwości ręcznego stworzenia instancji obiektów. Czym się zatem te twory różnią?

1. Singleton może implementować interfejs lub dziedziczyć po bazowej klasie. Jest to poważna zaleta w stosunku do klasy statycznej. Dzięki temu można np.:

class XnaRenderer: IRenderer
{
    //...

}
class XnaRenderer: BaseRenderer
{
    //...

}

Singleton w pełni wspiera polimorfizm, dziedziczenie. Krótko mówiąc, singleton jest dużo bardziej obiektowy niż klasa statyczna.

2.  Singleton to obiekt i można go przekazać jako parametr. Nie zawsze istnieje taka potrzeba ale czasami np:

void Init(IRenderer renderer)
{
//....
}
void Main()
{
    // Unit testing
    Render(MockRenderer.Instance);    

}

Tak jak w poprzednim punkcie, singleton wciąż jest bardzo obiektowy. Klasa statyczna jest całkowicie globalna.

3.  Singleton umożliwia testy jednostkowe. Powyższe oba punkty pokazały już, że nie stoi nic na przeszkodzie aby stworzyć mock\stub.

4.  Jeśli chcemy dokonać serializacji, singleton można zapisać do pliku używając dostępnego API: XMLSerializer, BinaryFormatter, SoapFormatter itp.

5. Jeśli zależy nam na kontroli liczby instancji, singleton łatwo można przerobić. Klasa styczna jest zawsze jedna i nie da się tego wykonać. W niektórych systemach istnieje potrzeba stworzenia kilku instancji a nie tylko jednej.

Jeśli ktoś z Was zna jakieś inne przykłady, zachęcam do komentowania. Osobiście unikam klas statycznych jak mogę.

Code review: lista danych

Rozważmy następujący kod:

private IList<int> ParseString(string text)
{
  string[] numbersStr = text.Split(';');
  var numbers=new List<int>();

  foreach (string numberStr in numbersStr)
  {
      numbers.Add(Int32.Parse(numberStr));
  }
  return numbers;
}

Co jest największym problemem tego kodu? Wszystkie przykłady wymyślam na bieżąco dlatego oprócz głównego problemu, który chce zaprezentować w poście, istnieje kilka pobocznych. Na przykład:

1.  Średnik w funkcji Split nie powinien być zahardcodowany.

2. Zamiast Int32.Parse warto zastanowić się nad sprawdzeniem czy na pewno mamy do  czynienia z liczbą.

3. Prawdopodobnie użycie LINQ byłoby bardziej czytelne.

4. Warto zastanowić się czy nie lepiej zwracać IEnumerable zamiast IList.

Jak wspomniałem jednak, stworzyłem taki kod aby pokazać naprawdę duży problem, który jest często popełniany już w produkcyjnym kodzie.

Jak działa dynamiczna kolekcja danych List? Tak naprawdę jest to niestety zwykła tablica, która zmienia swój rozmiar jeśli to tylko potrzebne. Początkowo tablica ma pojemność 0. W momencie dodania pierwszego elementu pojemność ustawiania jest na 4. Po przekroczeniu 4 rozmiar jest podwajany do 8, potem do 16 itp. Realokacja tablicy jest bardzo wolnym procesem, ponieważ należy stworzyć nową tablicę a potem element po elemencie  skopiować zawartość. Jeśli znamy rozmiar docelowy tablicy warto przekazać pojemność w konstruktorze:

private static IList<int> ParseString(string text)
{
  string[] numbersStr = text.Split(new[]{';'},StringSplitOptions.RemoveEmptyEntries);
  var numbers=new List<int>(numbersStr.Length);

  foreach (string numberStr in numbersStr)
  {
      numbers.Add(Int32.Parse(numberStr));
  }
  return numbers;
}

W powyższy sposób uniknęliśmy ciągłej realokacji. Ponadto nie musimy alokować pamięci, której nie potrzebujemy – pamiętajmy, że automatycznie rozmiar jest podwajany.

Aby udowodnić to napiszmy jeszcze prosty programik pokazujący jak zmienia się pojemność listy:

 var numbers = new List<int>();

  Console.WriteLine(numbers.Capacity); // Pojemność: 0

  numbers.Add(1);

  Console.WriteLine(numbers.Capacity); // Pojemność: 4

  numbers.Add(1);
  numbers.Add(1);
  numbers.Add(1);
  numbers.Add(1);

  Console.WriteLine(numbers.Capacity); // Pojemność: 8

  numbers.Add(1);
  numbers.Add(1);
  numbers.Add(1);
  numbers.Add(1);

  Console.WriteLine(numbers.Capacity); // Pojemność: 16

Z jawną alokacją wygląda to następująco:

var numbers = new List<int>(16);

Console.WriteLine(numbers.Capacity); // Pojemność: 16

numbers.Add(1);

Console.WriteLine(numbers.Capacity); // Pojemność: 16

numbers.Add(1);
numbers.Add(1);
numbers.Add(1);
numbers.Add(1);

Console.WriteLine(numbers.Capacity); // Pojemność: 16

numbers.Add(1);
numbers.Add(1);
numbers.Add(1);
numbers.Add(1);

Console.WriteLine(numbers.Capacity); // Pojemność: 16

Method hiding w c# – kiedy używać?

W c# słowo kluczowe ‘new’ nie służy wyłącznie do alokacji zasobów. W klasach istnieje koncepcja wirtualnych metod, które deklaruje się za pomocą słowa virtual. Są to podstawy polimorfizmu więc w tym poście nie będę opisywał już słówka virtual a wyłącznie new, które używa się również w połączeniu z metodą.

Metoda oznaczona new po prostu przykrywa metodę bazową. Najlepiej wyjaśnić to na przykładzie 2 klas:

class Cat:Animal
{
   public void Print()
   {
       Console.WriteLine("Cat");
   }
}
class Animal
{
   public void Print()
   {
       Console.WriteLine("Animal");
   }
}

Nie mamy słówka virtual ani override. Po prostu zadeklarowaliśmy dwie takie same metody. Kod się skompiluje jednak z pewnym warning: “‘ConsoleApplication2.Cat.Print()’ hides inherited member ‘ConsoleApplication2.Animal.Print()’. Use the new keyword if hiding was intended”. Aby uniknąć tego należy oznaczyć metodę słówkiem new:

class Cat:Animal
{
   public new void Print()
   {
       Console.WriteLine("Cat");
   }
}
class Animal
{
   public void Print()
   {
       Console.WriteLine("Animal");
   }
}

Jeszcze raz zaznaczam, ze oba fragmenty kodu się skompilują i będą działały identycznie. Słówko new jednak jest zalecane aby jawnie powiedzieć, że metoda przysłania inną metodę.

Co to znaczy jednak przysłonięcie? Przysłonięcie to przeciwieństwo polimorfizmu. Teraz typ zmiennej decyduje, która implementacja Print zostanie wykonana a nie na co wskaźnik wskazuje. Przykład:

Animal animal = new Cat();
animal.Print();

Gdybyśmy wykorzystali polimorfizm, Print wyświetliłby tekst “Cat’”. W tym jednak przypadku, ujrzymy “Animal”  bo takiego typu jest zmienna.

Trzeba przyznać, że jest to dziwne i nieeleganckie. Dlaczego Microsoft zdecydował się na wprowadzenie tego w c#? Starałem się znaleźć jakieś praktyczne przykłady. Osobiście nigdy nie musiałem z tego skorzystać ale faktycznie w .NET Framework są przykłady użycia “new”. Zacznijmy od najbardziej popularnego:

interface IEnumerable<T> : IEnumerable {
  new IEnumerator<T> GetEnumerator();
}

Foreach używa GetEnumerator do zwrócenia interfejsu IEnumerator. Problem w tym, że kiedyś istniała wyłącznie metoda zwracającą typ object a w .NET 2.0 wprowadzano generics. W generycznym interfejsie chcemy zatem przykryć starą implementację – w interfejsie zadeklarujemy metodę o identycznej  nazwie i tych samych parametrach.

Drugim przykładem jest klasa SQLConnection która dziedziczy po DBConnection. DBConnection ma takie metody jak np.:DbCommand CreateCommand(). Z kolei SQLConnection ma taką samą metodę z tym, że zwraca ona inny typ (SQLCommand). Rozważmy podobny, uproszczony przypadek:

public class DbConnection
{
    public DbCommand Command{get;}
}
public SqlConnection
{
    new public SqlCommand Command{get{return (SqlCommand)Command;}}
}

Dla instancji SqlConnection warto stworzyć hiden method, która zwraca już obiekt po rzutowaniu. Dzięki temu, mamy specyficzne dla SqlCommand metody i nie musimy np.:

SqlConnection connection = new SqlConnection();
SqlCommand cmd = (SqlCommand)connection.Command;
cmd.SqlSeverMethod();

Z metodą przysłaniającą wystarczy:

SqlConnection connection = new SqlConnection();
connection.Command.SqlServerMethod();

Konwersja (a konkretnie kowariancja) jest najpopularniejszym scenariuszem użycia.

Na jednym z forum znalazłem jednak jeszcze inny przykład, wynikający z błędnej architektury kodu niezależnego od nas. Wyobraźmy sobie, że klasa Employee zawiera pole Salary i przekazujemy instancję tej klasy do kontrolki PropertyGrid. Nie chcemy jednak wyświetlać tej właściwości w kontrolce. Wtedy z jednym rozwiązań jest przysłonięcie tej właściwości i dodanie atrybutu:

public class Manager : Employee
{
    [Browsable(false)]
    public new int Salary
    {
        get { return base.Salary; }
        set { base.Salary = value; }
    }
}

Jeśli ktoś zna inne przykłady, zachęcam do podzielenia się tą wiedzą w komentarzach.

Obiekty niezmienne – immutable objects

Immutable objects to obiekty w inżynierii oprogramowania, które pozostają niezmienne po ich inicjalizacji. Wszystkie typy numeryczne, struktury oraz inne value type są immutable. Istnieją również klasy, które zachowują się jak typy niezmienne. Spróbujmy opisać kilka ważnych cech tych obiektów, które mają dla nas specjalne znaczenie:

  1. Obiekty niezmienne (immutable) są thread-safe – przystosowane są do dostępu współbieżnego. Skoro obiekt już skonstruowany nie może zostać zmodyfikowany to nie musimy się kompletnie martwić o synchronizację, deadlock, livelock, starvation itp. Operacje odczytu danych są bezpieczne a tylko takie na obiektach niezmiennych mogą zostać wykonane.
  2. Niezmienne obiekty świetnie nadają się na klucze w słownikach lub HashSet. W HashSet kluczem jest nie tyle co obiekt a hashcode. HashCode liczony jest na podstawie zawartości obiektu. W niezmiennych obiektach mamy pewność, że zawartość nie zostanie zmodyfikowana i tym samym klucz zawsze będzie prawdziwy – nietrudno wyobrazić sobie sytuację w której najpierw dodajemy do HashSet obiekt (liczony jest hashcode), potem modyfikujemy jego zawartość tym samym nie aktualizując HashCode w HashSet.
  3. Operacje przypisania są dużo łatwiejsze np:
var sample = new ImmutableObject(5,3,10);
ImmutableObject sample2 = sample;

W przypadku typów referencyjnych, wykonanie kopii (a dokładniej deep copy) jest bardzo kosztowne  – należy zadeklarować nowy obiekt i przekopiować wszystkie elementy składowe obiektu. W przypadku immutable objects nie ma takiej potrzeby. W końcu skoro mamy pewność, że obiekt nie zostanie zmodyfikowany to nie ma znaczenia, że  w pamięci oba wskaźniki (sample, sample2) wskazują na taki sam adres.

Jak widać, niezmienne obiekty są bezpieczniejsze i czasami przynoszą oszczędności w pamięci. W .NET nie magicznego atrybutu lub interfejsu wymuszającego na klasie niezmienność.  Projektując jednak tego typu obiekty, należy rozważyć następujące kroki

  1. Wszelkie pola oznaczyć modyfikatorem const lub readonly.
  2. Jeśli wiemy, że klasa nie będzie miała klas pochodnych wtedy warto oznaczyć ją jako sealed.
  3. Wszelkie właściwości muszą być pozbawione setter’a.

Przykład:

public class User
{
    private readonly string _name;
    private readonly UserType _type;
    
    public User(string name, UserType type)
    {
        _name = name;
        _type = type;
    }
    public string Name{get{return _name;}}
    public UserType UserType{get{return _type;}}
    
    public User PromoteTo(UserType userType)
    {
        return new User(_name, userType);
    }
}

W klasycznych obiektach, metoda PromoteTo modyfikowałby po prostu właściwość UserType. W przypadku immutable, musimy stworzyć nowy obiekt – jakiekolwiek modyfikowanie stanu obiektu jest zabronione. Oznaczając pola jako readonly, mamy pewność, że po skonstruowaniu obiektu, nie będzie można już zmienić ich zawartości.

Immutable objects jest bardzo popularnym terminem w świecie Java. W środowisku .NET, myślę, że nie jest to tak spopularyzowane. Z tego względu warto pokazać kilka przykładów występujących w .NET Framework.

Najsłynniejszym przykładem jest klasa (to nie jest struktura!) String. Zachowuje się ona całkowicie jak value type. Został w niej przeładowany operator przypisania, który zwraca zawsze kopie strumienia znaków a nie po prostu adres komórki jak to jest domyślnie w typach referencyjnych. Wszystkie operacje np. ToLower(), Trim() zwracają nowy obiekt a nie modyfikują już istniejący – tak samo jak to przed chwilą zaimplementowaliśmy w User: PromoteTo. Nie ma zatem możliwość zmodyfikowany tablicy char już po utworzeniu instancji string. Wszystkie metody zwracają po prostu nowy obiekt a właściwości nie mają setter’a.

Wszelkie delegaty to również tak naprawdę obiekty niezmienne. Rozważmy następujący kod:

class Program
{
   delegate int AddOperation(int x, int y);

   static void Main(string[] args)
   {
       AddOperation addOperation = (x, y) => x + y;

       int result = addOperation(2, 5);
   }
}

Po skompilowaniu do IL, generowany jest immutable object:

.class auto ansi sealed nested private AddOperation
    extends [mscorlib]System.MulticastDelegate
{
    .method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed
    {
    }

    .method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(int32 x, int32 y, class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed
    {
    }

    .method public hidebysig newslot virtual instance int32 EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
    {
    }

    .method public hidebysig newslot virtual instance int32 Invoke(int32 x, int32 y) runtime managed
    {
    }
}

 

Innym przykładem są typy anonimowe:

class Program
{
   delegate int AddOperation(int x, int y);

   static void Main(string[] args)
   {
       var anonymousType = new {x = 5, y = 10};
       int x = anonymousType.x; // OK
       anonymousType.x = 10; // BLAD
   }
}

Jak widać po skonstruowaniu obiektu, nie ma możliwości już zmodyfikowania jego stanu.

AppDomain–część II

Dzisiaj pokażę, jak od strony programistycznej wygląda AppDomain. Zwykle tworzymy aplikację host, która trzyma referencje do kilku AppDomain. Stwórzmy najpierw aplikację konsolową wyświetlającą po prostu tekst:

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            while (true)
            {
                Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
                System.Threading.Thread.Sleep(2000);
            }
        }
    }
}

Jeśli uruchomimy aplikację, domyślnie oczywiście będzie działała w osobnym procesie. Z tego względu CurrentDomain.FriendlyName wyświetli ConsoleApplication3.exe – nazwę procesu.

ConsoleApplication3 uruchomimy w osobnym AppDomain. Przejdźmy do aplikacji hostującej, która stworzy AppDomain i załaduje potrzebne zasoby.

Aby stworzyć podstawową domenę, wystarczy wywołać statyczną metodę AppDomain.CreateDomain:

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain appDomain=AppDomain.CreateDomain("Nazwa AppDomain");
            appDomain.ExecuteAssembly(@"ConsoleApplication3.exe");            
        }
    }
}

ExecuteAssembly działa w sposób synchroniczny, czyli blokuje dalsze wywołanie kodu aż do momentu zakończenia ConsoleApplication3. Jeśli zajrzymy w TaskManager, zobaczymy wyłącznie ConsoleApplication2.exe ponieważ ConsoleApplication3.exe został uruchomiony jako AppDomain a nie nowy proces. Jest to oczywiście znaczącą szybsze niż odpalanie nowego procesu a stopień izolacji pozostaje podobny. Po uruchomieniu na ekranie zobaczymy tekst “Nazwa AppDomain” a nie jak wcześniej “ConsoleApplication3.exe”.

ExecuteAssembly ładuje daną bibliotekę i wywołuje entry method. Istnieje również możliwość załadowania biblioteki i wykonanie konkretnej metody na danej klasie. Stwórzmy zatem bibliotekę i jakąś przykładową klasę:

public class PrintHelper : MarshalByRefObject
{
    public void Print()
    {
        Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
    }
}

Pierwsza nowość: dziedziczymy po MarshalByRefObject. Ze względu na fakt, że obiekty w dwóch różnych AppDomain są od siebie odizolowane, nie istnieje bezpośredni sposób komunikacji między nimi. Oczywiście gdybyśmy mogli bezpośrednio odwoływać się do klas w innych AppDomain, byłoby to z sprzeczne z ideą, którą od dwóch postów opisuję – izolacja i bezpieczeństwo. Najpopularniejszym sposobem komunikacji między domenami jest .NET Remoting. Istnieje również możliwość komunikacji za pomocą protokołu named pipes oraz WCF ale to temat na inny post. Komunikujemy się zatem w sposób pośredni. MarshalByRefObject mówi, że dla obiektu zostanie stworzony proxy. Klient zatem będzie korzystał nie z PrintHelper a z proxy, który wywołuje metody na zadanej AppDomain. W świecie .NET remoting ma to taką wadę, że w przypadku wielu wywołań za każdym razem musimy wysyłać żądanie obciążając tym samym sieć. Drugim sposobem jest marshal by value czyli przekazanie kopii obiektu do klienta i wykonanie zadanej logiki po stronie klienta. Jeśli chcemy korzystać z takiego podejścia nie musimy dziedziczyć po żadnej klasie – wystarczy doczepić atrybut Serializable. Decyzja zależy od scenariusza – jeśli nie mamy dużą wywołań lepiej skorzystać z MarshalByRefObject. Z kolei dla małych obiektów z wysoką liczbą wywołań wydajniejszym rozwiązaniem jest serializacja\deserializacja (marshal by value).

Wiemy już, że do komunikacji między AppDomain musimy skorzystać ze specjalnych mechanizmów. Przejdźmy do aplikacji hostującej, która chce wywołać PrintHelper w innej domenie:

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain appDomain = AppDomain.CreateDomain("Nazwa AppDomain");

            var domainHelper =
                (PrintHelper)
                appDomain.CreateInstanceAndUnwrap(typeof (PrintHelper).Assembly.FullName, typeof(PrintHelper).FullName);
            domainHelper.Print();

            PrintHelper processHelper = new PrintHelper();
            processHelper.Print();
        }
    }
}

CreateInstanceAndUnwrap służy do stworzenia oraz wykonania unwrap (o tym później) na obiekcie – na skutek czego dostaniemy po prostu instancję obiektu (w tym przypadku proxy). domainHelper wykonuje kod w innej domenie i wywołanie metody Print wydrukuje “Nazwa AppDomain”. Z kolei processHelper to zwykła instancja i wywołanie Print zwróci nazwę aktualnego procesu (ConsoleApplication2). Aby przekonać się, że domainHelper to faktycznie proxy wystarczy np. zajrzeć do debuggera a ujrzymy jako typ “{System.Runtime.Remoting.Proxies.__TransparentProxy}”. Po usunięciu MarshalByReflectObject i dodaniu atrybutu Serializable ujrzymy prawdziwą nazwę obiektu ponieważ został on całkowicie skopiowany i nie ma potrzeby używania proxy. Ale zastanówmy się co właśnie zrobiliśmy…Nakazaliśmy skopiować cały obiekt i przesłać go do klienta. A co z izolacją? Niestety to co zrobiliśmy było głupie ponieważ kod zostanie wykonany na ConsoleApplication2.exe a nie na domenie “Nazwa AppDomain”.  Marshal by value lepiej pozostawić dla kontenerów służących np. jako parametry. W pozostałych przypadkach korzystajmy z MarshalByRefObject (jak w powyższym kodzie).

Następna kwestia to operacja Unwrap. Aby wyjaśnić, przyjrzyjmy się drugiej metodzie do tworzenia obiektów CreateInstance:

ObjectHandle handler=appDomain.CreateInstance(typeof (PrintHelper).Assembly.FullName, typeof(PrintHelper).FullName);

var pritnHelper = (PrintHelper) handler.Unwrap();

CreateInstance zawraca ObjectHandler, który jest wrapperem naszego obiektu. Aby dostać się do prawdziwego obiektu wystarczy wywołać UnWrap. Wcześniej użyta metoda CreateInstanceAndUnwrap wykonuje te dwie operacje od razu. Podstawowe pytanie jednak brzmi: po co tak kombinować z tym wrapperem? Nie można byłoby zawsze zwracać właściwy obiekt? Można byłoby, ale nie zawsze jest to wydajne. W końcu musimy albo stworzyć proxy lub wykonać dokładną kopię obiektu. Są to operacje dość czasochłonne a nie zawsze chcemy je od razu wykonywać. W końcu możemy stworzyć ObjectHandle na początku, potem go przekazywać przez jakieś parametry metod a dopiero na końcu wykonać metodę.

Jak już wspomniałem  w poprzednim poście, bibliotek w środowisku .NET nie da się usunąć z pamięci. Możliwe jednak jest usunięcie AppDomain wraz z załadowanymi w niej bibliotekami. Z tego względu metoda AppDomain.Unload jest bardzo ważna z punktu widzenia zużycia pamięci:

AppDomain.Unload(appDomain);

Na zakończenie warto dodać, że to tylko początek (jak to zwykle bywa). Często tworzy się AppDomain aby wykonać jakiś kod z użyciem innym przywilejów.  W momencie tworzenia nowego AppDomain można przekazać dodatkowe, konfiguracyjne  parametry:

AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = System.Environment.CurrentDirectory;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

Evidence baseEvidence = AppDomain.CurrentDomain.Evidence;
Evidence evidence = new Evidence(baseEvidence);

AppDomain ad2 = AppDomain.CreateDomain(name, evidence, ads);

Co to jest AppDomain?

W .NET istnieje twór zwany AppDomain. W systemie operacyjnym mamy kilka podobnych pojęć takich jak proces czy wątek. AppDomain w dużej mierze przypomina proces – służy do izolowania aplikacji bezpośrednio niezwiązanych ze sobą. Dwa różne procesy mają przede wszystkim różną przestrzeń pamięciową. Adres zmiennej w procesie A, odnosi się już do innego obszaru w procesie B – alokacja zaczyna się w różnych miejscach.  Z tego względu uruchomienie nowego procesu jest czasochłonną operacją i wiąże się z alokacją pewnych zasobów.

Czym różni się zatem AppDomain od procesu? Wiemy, że stopień izolacji jest podobny do procesu. Różnica polega na tym, że jeden proces może mieć kilka AppDomain. Poza tym AppDomain jest znacznie szybszy od procesu i jego uruchomienie nie wymaga alokacji aż tylu zasobów. Zatem jeśli ze względów bezpieczeństwa chcemy wykonać jakiś kod w izolacji, lepiej uruchomić nowy AppDomain a nie proces. Ponadto, jak wiemy, bibliotek załadowanych nie można ponownie usunąć z tego samego procesu. Za pomocą AppDomain można uniknąć tego problemu: tworzymy nowy AppDomain, ładujemy potrzebne biblioteki, wykonujemy kod a na końcu usuwamy AppDomain wraz z wszystkimi nadmiarowymi bibliotekami.

Jeśli piszemy aplikację serwerową i chcemy aby każdy request został wykonany w środowisku odizolowanych wtedy warto zastanowić się nad uruchamianiem AppDomain – jest to dużo szybsze niż uruchamianie nowego procesu. Podsumowują dzięki AppDomain :

  1. Kod w jednym AppDomain nie wpływa na drugi – jakakolwiek awaria nie wpływa na wykonanie kodu w innej domenie.
  2. Istnieje możliwość wyładowania z pamięci kodu, nie kończąc tym samym działania całej aplikacji.
  3. Kod z jednego AppDomain nie może korzystać ani odwoływać się do zasobów drugiej domeny.
  4. Pozwolenia mogą być nadawane niezależnie.

Tak jak w procesach i w przeciwieństwie do wątków jeden AppDomain nie może odwoływać się do zmiennych z drugiego AppDomain. Istnieje jednak pewien sposób komunikacji ale o tym w przyszłym poście – podobny jest do zdalnej komunikacji a nie do bezpośredniej adresacji.

To tyle teoretycznego wstępu. W następnym poście pokażę jak to wygląda od strony programistycznej i API.

Programowanie asynchroniczne w .NET 4.5

.NET 4.5 przynosi wiele zmian. Szczególnie ciekawym jest nowe podejście do programowania asynchronicznego, znacząco ułatwiające prace programisty z callback’ami itp. W zasadzie nie ma już callback’ow – kod asynchroniczny niewiele różni się od synchronicznego. Zachęcam do przeczytania mojego nowego artykułu:

http://msdn.microsoft.com/pl-pl/library/programowanie-asynchroniczne-w-net-4-5.aspx