Immutability w .NET

Udostępnij

Wprowadzenie

Dzisiaj chciałbym pokrótce opisać mechanizmy immutability, które dostarcza .NET. Skupię się również na zmianach, jakie pojawiły się wraz z wprowadzeniem .NET Core. Immutability to ciekawy temat, a młodzi programiści najczęściej spotykają się z nim po raz pierwszy, ucząc się, jak działa String w C#. Dlaczego Immutability jest tak istotne? Im więcej kontroli mamy nad kodem, tym lepiej. Jeśli posiadamy dane, które nie powinny być modyfikowane, a dodatkowo kod może zapewnić ich ochronę, jest to sytuacja idealna!

Przykłady

Jednym z najprostszych mechanizmów do osiągnięcia immutability w C#, jest użycie słowa kluczowego readonly. O ile w przypadku typów prostych działa to bez zarzutu, w przypadku typów referencyjnych, readonly chroni jedynie referencję do obiektu, a nie same właściwości tego obiektu. Warto mieć to na uwadze.

// w przypadku próby modyfikacji, na przykład w metodzie, dostaniemy błąd. 
private readonly int _value = 5;

W przeszłości dla właściwości w C# mogliśmy ustawić początkową wartość i użyć jedynie gettera, jednak takie podejście nie oferowało zbyt dużej elastyczności. Z pomocą przychodzi słowo kluczowe init, które wprowadza większą kontrolę nad przypisywaniem wartości. Dzięki niemu możemy przypisać wartość początkową podczas inicjalizacji obiektu, a następnie uniemożliwić jej zmianę.

// stare podejście - mało elastyczne
public int Value { get; } = 15;
// użycie init
public int Value { get; init; }

Słowo kluczowe readonly można również zastosować w przypadku struktur w C#. Kiedy struktura zostanie oznaczona jako readonly, kompilator narzuca ograniczenia, gdzie nie jesteśmy w stanie przy właściwości użyć settera, możemy użyc tylko get lub init.

// W przypadku próby ustawienia wartości poza konstruktorem, dostaniemy błąd. 
public readonly struct Person
{
    public string Name { get; init; } // jeśli spróbujemy zrobić set; również dostaniemy błąd!
    public string Surname { get; init; }

    public Person(string name, string surname)
    {
        Name = name;
        Surname = surname;
    }
}

Kolejna rzecz która się pojawiła, to słowo kluczowe in, obok ref i out. Jest to trzeci sposób oznaczania parametrów metod, pozwalający na przekazywanie wartości tylko do odczytu.

public void Check(in int value)
{
    value = 15; // dostajemy błąd
}

Słowo kluczowe required w C# stanowi uzupełnienie dla init, które pozwalało na ustawienie wartości podczas inicjalizacji obiektu. Jednak init nie gwarantowało, że wartość właściwości zostanie faktycznie przypisana. Problem ten rozwiązuje właśnie required, które wymusza, aby właściwość była ustawiona przy tworzeniu obiektu.

public required string Name { get; init; }

Rekordy

Zaczyna robić się ciekawiej! Jeśli jeszcze nie znasz rekordów w c#, to mam nadzieję że pozytywnie się zaskoczysz! Rekordy to typy referencyjne, które działają bardzo podobnie do typów wartościowych. Łączą w sobie zalety typów referencyjnych, ale można je traktować jak typy wartościowe – co jest szczególnie przydatne na przykład przy porównywaniu danych. Na pierwszy rzut oka składnia może wydawać się nieco mało intuicyjna, ale to tylko kwestia przyzwyczajenia. Można je też tworzyć w sposób klasyczny, chociaż wymaga to trochę więcej kodu.

public record Person(string Name, string Surname);

var person1 = new Person("Jakub", "Wierzbanowski");
var person2 = new Person("Jakub", "Wierzbanowski");
Console.WriteLine(person1 == person2); // dostaniemy true ponieważ wartości są takie same!

Powyższy przykład tworzy rekord, którego właściwości są immutable, co oznacza, że nie możemy zmienić żadnej z nich po inicjalizacji. Jedyny minus, który tutaj może wywoływać niepokój, jest to, że parametry muszą być z dużej litery, żeby właściwosci miały „prawidłowe” nazwy. W tym wypadku, plusy przesłaniają minusy 😀 Możemy rozszerzać nasze rekordy o dodatkowe metody i właściwości, wszystko w zależności od potrzeb.

public record Person(string Name, string Surname)
{
    public string GetInitials() => $"{Name.FirstOrDefault()}{Surname.FirstOrDefault()}";
}

W przypadku próby modyfikacji rekordów, z pomocą przychodzi słowo kluczowe with, które pozwala na stworzenie nowego rekordu, kopiując istniejący z podmienionymi wybranymi polami.

var person1 = new Person("Jakub", "Wierzbanowski");
var person2 = person1 with { Name = "Dawid" };

Ciekawostka: Rekordy w C# automatycznie generują metodę ToString(), która zwraca reprezentację obiektu w czytelnej formie.

Console.WriteLine(person2);
// Person { Name = Dawid, Surname = Wierzbanowski }

Rekordy w C# mogą również być strukturami, dzięki czemu zyskują pełne korzyści płynące z używania typów wartościowych.

public record struct Person(string Name, string Surname)
{
    public string GetInitials() => $"{Name.FirstOrDefault()}{Surname.FirstOrDefault()}";
}
Czytaj również  LINQ - Kolejne usprawnienia
Scroll to Top