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()}";
}