Nowości w C# 9.0

.NET (Core) 5.0 wydany

Piotr Wandycz
Język C#

Kilka dni temu miał premierę .NET 5, a wraz z nim język C# 9.0. Idzie kilka ważnych zmian, włącznie z klasycznym cukrem składniowym. Największą niespodzianką jest dla mnie nowy rodzaj typu referencyjnego wartościowego – rekord. Dotnet zaczyna nam ułatwiać tworzenie niezmiennych obiektów, czy ich właściwości. Możliwe, że idzie to trochę w kierunku programowania funkcyjnego. Nie przedłużając – oto najbardziej rzucające się w oczy zmiany. Resztę muszę doczytać 🙂

Instrukcje najwyższego poziomu

Na początek zobaczmy, co zmienia się w pisaniu programów konsolowych, bo może pojawić się trochę dziwnego kodu. Po staremu przykładowa aplikacja wyglądała tak:

using System;

namespace LanguageFeatures
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            Console.ReadKey();
        }
    }
}

Teraz nie ma problemu, żeby taki kod się skompilował:

using System;

Console.WriteLine("Hello World!");
Console.ReadKey();

Kompilator dopuszcza jeden plik używający ‘top-level statements‘, więc przy utworzeniu kolejnego pliku z powyższym kawałkiem kodu powinien pojawić się błąd. Sprawdziłem: jest.

Co więcej – możemy wykorzystać wprowadzone w C# 7.0 funkcje lokalne i pisać naprawdę małe i proste programy w jednym pliku. Jak jest napisane w dokumentacji Microsoftu – jednym z najczęstszych zastosowań tej funkcjonalności ma być ułatwienie nauczania języka, a nawet umożliwienie nowym programistom napisanie ‘hello world’ w jednej linijce. Po co komplikować komuś naukę na starcie i tłumaczyć co robi Main() i czemu jest tam jakieś string[] args, gdy on/ona nie wie nawet, co oznacza pojęcie zmiennej?

Mamy dalej możliwość przyjmowania parametrów i zwracania exit code, ale w tym przypadku już użyłbym pełnej składni, bo skrócona – mimo że się kompiluje – utrudnia mi mocno zrozumienie kodu.

using System;

if (args?.Length > 0)
{
    foreach (var arg in args)
    {
        Console.WriteLine(arg);
    }
}

return 1;

Spytałem parę osób o zdanie i raczej nie są zachwycone możliwością stosowania ‘instrukcji najwyższego poziomu’. Dopiero dla osób zaczynających swoją przygodę z .NET od wersji 5.0 będzie to normalny zapis. Kolejne zmiany są mniej kontrowersyjne.

Nowy typ danych

O ile ‘instrukcje najwyższego poziomu’ są jedynie ciekawostką, C# 9.0 przynosi nam nowy typ danych – record. Po wstępnej analizie wygląda on na coś w rodzaju Value Object z Domain Driven Design. Jest to typ referencyjny, ale przy porównywaniu dwóch obiektów brane pod uwagę są wartości pól. Dodatkowo jest on niezmienny (immutable), więc jego właściwości nie posiadają setterów. Klasa zdefiniowana w następujący sposób:

class Fruit
{
    public string Name { get; }

    public Fruit(string name)
    {
        Name = name;
    }
}

Może zostać teraz zapisana jako:

record Fruit(string name);

A przy porównywaniu dwóch obiektów brane pod uwagę są ich wartości, a nie referencja (miejsce przechowywania w pamięci):

Jest to ogromne usprawnienie, bo nie będzie trzeba pisać masy kodu porównującego poszczególne właściwości danej klasy poprzez implementację interfejsu IEquatable, żeby sprawdzić, czy dwa obiekty są takie same.

Jeśli koniecznie zależy nam, aby rozszerzyć rekord o settery – możemy to zrobić. Definicja wygląda jak normalnej klasy poza tym, że w dalszym ciągu zachowujemy możliwość porównania właściwości obiektu przez wartości.

class Program
{
    static void Main(string[] args)
    {
        var f = new Fruit("cherry");
        f.name = "a"; // error

        var fm = new FruitMutable("cherry");
        fm.Name = "a"; // ok
    }
}

record Fruit(string name);

record FruitMutable
{
    public string Name { get; set; }
    public FruitMutable(string name)
    {
        Name = name;
    }
};

Ale możemy też skorzystać z ‘pattern matching‘ i zrobić taką konstrukcję:

class Program
{
    static void Main(string[] args)
    {
        var f = new Fruit("cherry");
        f = f with { name = "a" };
    }
}

record Fruit(string name);

Dla mnie jest to ogromna zmiana na plus, bo będzie można pisać mniej kodu w komendach, czy innych DTOsach, które powinny działać na zasadzie get-only.

using MediatR;

public record Command(int questionId, int userId, string answer) : IRequest { }

Niezmienny setter

Kolejnym ułatwieniem pisania niezmiennych (immutable) właściwości jest dodanie słowa kluczowego init, które możemy zastosować zamiast słowa set.

class Program
{
    static void Main(string[] args)
    {
        var fruit = new Fruit { Name = "cherry" };
        fruit.Name = "x"; // error
    }
}

class Fruit
{
    public string Name { get; init; }
}

Dzięki init mamy możliwość użycia inicjatora obiektu (przypisanie właściwości w klamrach powyżej). Po staremu, żeby uzyskać podobny efekt, musielibyśmy na przykład utworzyć pole, skorzystać z readonly i ustawienia wartości przez konstruktor.

class Program
{
    static void Main(string[] args)
    {
        var fruit = new Fruit("cherry");
        fruit.Name = "x"; // error
    }
}

class Fruit
{
    public readonly string Name;

    public Fruit(string name)
    {
        Name = name;
    }
}

Usprawnienia pattern matching

Rozpoznawanie wzorców (chociaż w tym przypadku lepiej używać angielskiej nazwy) także doczekało się usprawnień. Bardzo często sprawdzałem nulla poprzez użycie instrukcji is null, a w końcu dostaniemy możliwość sprawdzenia is not null. Pozostałe rzeczy – and, or, not i operatory porównania lepiej zobaczyć w kodzie, niż opisywać. Zmiana na plus moim zdaniem.

using System;

class Program
{
    static void Main(string[] args)
    {
        var factory = new FruitFactory();
        var fruit = factory.CreateFruit("cherhy");
        if(fruit is not null)
        {
            Console.WriteLine("fruit exists");
        }
    }
}

record Fruit(string name);

class FruitFactory
{
    public Fruit CreateFruit(string name)
    {
        if(name is "cherry" or "apple")
        {
            return new Fruit(name);
        }
        else if(name.Length is >= 3 and <= 7 and not 5)
        {
            Console.WriteLine("thats a really weird factory");
            throw new NotImplementedException();
        }
        else
        {
            throw new NotImplementedException();
        }
    }
}

Mam nadzieję, że nie napisałem tu zbyt wielu głupot, bo dopiero następne lata pokażą mi pełny wachlarz zastosowań tych zmian. C# zaczął chyba iść w kierunku bardziej funkcyjnym, ale to musiałby mi potwierdzić ktoś, kto programuje funkcyjnie. Wybrałem tu nieco bardziej praktyczne nowości, ale zachęcam Cię do sprawdzenia pełnej listy na stronie Microsoftu.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *