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.

6 thoughts on “Obiekty niezmienne – immutable objects”

  1. Innymi słowy piękny artykuł o wzorcu ValueObject w DDD, przyznaje, że pewne rzeczy mi się dzięki temu tekstowi bardziej “wyjaśniły” :). Ale faktem jest, że w świecie .NET wymaga to popularyzacji…

  2. Minusem chyba jest większe obciążenie GC, który ma dzięki temu więcej pracy. Są jakieś jeszcze ciemne strony tego podejścia?

  3. Piotrze, w powyższym wpisie napisałeś:

    “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.”

    Odnośnie operatora przypisania, to chyba nie jest to prawdą (albo ja tutaj czegoś nie rozumiem). Mam taki kod:

    var s1 = “test”;
    var s2 = s1;
    Console.WriteLine(ReferenceEquals(s1, s2));

    Na konsoli otrzymuje True, co oznacza, że jednak nie dostajemy kopii obiektu, tylko zwykłą kopię referencji (czyli String zachowuje się tutaj jak każdy inny typ referencyjny).

    Możesz to jakoś skomentować?

  4. Dla przykładu
    var s1 = “test”;
    var s2 = s1;
    Console.WriteLine(ReferenceEquals(s1, s2));
    zwraca true z tego powodu, że są to stringi Interned

Leave a Reply

Your email address will not be published.