9.0 W listopadzie 2020 pojawiły się .NET 5 i C# 9.0 . Z ciekawości Ci powiem, że zdarzyło mi się mieć rozmowy kwalifikacyjne na temat C# 9.0 już tydzień po premierze więc czytanie o nowościach języka nigdy nie wiesz, kiedy może Ci się przydać. 

Zmotywowało mnie to do zrobienia webinaru na ten temat.

Miałem też inną rozmowę, na której dwaj architekci oczekiwali ode mnie napisania "Pattern Matching" z buta.  Trzeba przyznać, że jest to ciekawy problem, ponieważ w firmach pisanie kodu w najnowszych technologiach jest niesamowitym luksusem. Dlatego nie dziw się, jeśli ktoś nie wie co pojawiło się w C# 8.0 albo C# 7.0 ?

C# 9.0 ma nowego? Najważniejsze nowości to rekordy i rozwinięcie możliwości Pattern Matching. Przejdźmy jednak po kolei do każdej nowości języka C#?

Top Level Calls

Jak wiesz prosty kod aplikacji konsolowej wymaga dużo kodu rytualnego. 

using System;

namespace ConsoleTopLevelCallsNot
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            Console.WriteLine(Substract(33, 11));

        }

        private static double Substract(double x, double y)
        {
            return x - y;
        }
    }
}

Ktoś stwierdził, a gdyby tak kod można by było napisać w C# jakby to był Python? Po prostu w każdej linijce piszę co mój program ma robić.

using System;

Console.WriteLine("Hello World!");
Console.WriteLine(Substract2(33, 11));

static double Substract2(double x, double y)
{
    return x - y;
}

Od teraz w C# 9.0 jest to możliwe. Istnieją oczywiście pewne ograniczenia. Na początku kodu nie możesz deklarować klas i metod. Te rzeczy deklarujesz po swoim kodzie.

Czy musisz gdzieś mówić, że ta klasa jest napisana w takim stylu? Nie 

Kompilator sam to stwierdzi. Istnieje jednak ograniczenie i w całym projekcie może być taka tylko jedna klasa w jednym pliku.

dwa pliki z top level calls

Oto przykład próby użycia "Top Level Calls" dwa razy.

animacja kodu do top level calls c# 9

Krótkie podsumowanie "Top Level Calls" . Co to robi ? 

  • Usuwanie ceremonii
  • Ułatwienie utworzenie aplikacji konsolowej
  • Jeżeli się uczysz C# to lepiej nie korzystać z tej funkcji
  • Zaraz a gdzie jest args[] ? No właśnie ?

Jak wiesz do aplikacjach konsolowych możesz przekazywać parametry wykonawcze.

using System;

namespace ConsoleTopLevelCallsNot
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                System.Console.WriteLine("Please enter a numeric argument.");
            }
        }
    }
}

Mimo iż Visual Studio twierdzi, że "args" nie istnieje to możesz się do niego odwołać w Top Level Calls.

using System;

if (args.Length == 0)
{
    System.Console.WriteLine("Please enter a numeric argument.");
}

Dla pewności masz tutaj obrazek z wykonywanego programu w Visual Studio.

Top Level Args istnieje

Initial Setters

Wyobraź sobie, że masz taką klasę, która reprezentuje model Pirata z bazy danej.

public class PirateModel
{
    public int Id { get; set; }

    public string CrewName { get; set; }
                    
    public string Name { get; set; }
}

Nie chciałbyś, aby inny programista mógł w tym modelu zmienić jego ID. Czyli "ID" powinno być deklarowane tylko raz.

public static class InitialSetterExample
{
    public static void Run()
    {
        PirateModel m = new PirateModel
        { Name = "Luffy", CrewName = "StrawHats", Id = 1 };
                    
        m.Id = 2;
        m.Name = "Zorro";
                    
        Console.WriteLine($"Hello {m.Name}, Id : {m.Id}");
    }
}

Jak się przed tym zabezpieczyć? Mógłbyś ustawić "set" na prywatny, tylko wtedy, jeśli chcesz go ustawić to albo potrzebujesz konstruktora, albo oddzielnej metody.

public class PirateModel
{
    public int Id { get; private set; }
                    
    public string CrewName { get; set; }
                    
    public string Name { get; set; }
}

W sumie to te rozwiązanie w żaden sposób Ci nie pomaga.

public class PirateModel
{
    public int Id { get; private set; }
                    
    public string CrewName { get; set; }
                    
    public string Name { get; set; }
                    
    public void UpdateId(int newId)
    {
        Id = newId;
    }
}

Na szczęście w C# 9.0 masz słowo kluczowe "init", które deklaruje daną właściwość tylko do jeden deklaracji. 

public class PirateModel
{
    public int Id { get; init; }
                    
    public string CrewName { get; set; }
                    
    public string Name { get; set; }
}

Teraz gdy inny programista będzie próbował nadpisać twoje ID to mu to nie wyjdzie. Właściwość stała się wartością niezmienną.

InitialSetters

Krótsza wersja tworzenia klas

Chciałbyś tworzyć klasy jeszcze krócej. To teraz możesz bez pisania całej nazwy klasy?

PirateModel m3 = new();

PirateModel m4 = new()
{ Name = "Zorro", CrewName = "StrawHats", Id = 2 };

Oczywiście nie możesz użyć takiej składni mając słowo kluczowe "var", ponieważ wtedy kompilator nie wie o co ci chodzi.

//var m5 = new();

Konrad Kokosa dodał wspaniałego Twitta pokazujący jak tą składnie wykorzystać do wkurzających pytań rekrutacyjnych. 

Konrad Kokosa Twitter

Dodatkowo tą nową składnią można stworzyć nieczytelny kod.

PirateModel m6;

//1000 linjek kodu później

m6 = new();

Pattern Matching w switch

Pattern Matching to potężna funkcjonalność dla większości programistów c# wywołuje ona atak euforii i radości. W końcu, zamiast pisać tonę "if, else" lub "switch, case" możesz teraz to wszystko skrócić odpowiedni wyrażeniem.

Aby to wszystko wyjaśnić potrzebujemy jakichś przykładów. Standardowo będę pokazywał Ci klasy, z jakich korzystam...oraz

public class Weather
{
    public DateTime Date { get; set; }
                    
    public int TemperatureC { get; set; }
                    
    public string Desc { get; set; }
}

Kod generujący losowe obiekty do mielenia przez te nowe cudowne wyrażenia warunkujące przypływ poleceń w kodzie 

public static List<Weather> CreateExample()
{
    DateTime now = DateTime.Now;
    var rng = new Random();
                
    var exampleCollection = Enumerable.Range(1, 14).Select
        (index => new Weather
        {
            Date = now.AddDays(index),
            TemperatureC = rng.Next(-10, 40)
        }
    ).ToList();
                
    return exampleCollection;
}

Zamiast pisać IF-y, els-y teraz możesz napisać taką logikę przepływu programu, która jest zależna od podanej temperatury.

foreach (var weather in exampleList)
{
    weather.Desc = weather.TemperatureC switch
    {
        <= 6 and > 1 => "Nie ciekawie. Ubrać sie trzeba",
        10 or 9 or 8 or 7 => "Zimno",
        11 => "Uwaga Łatwo zachorować",
        12 => "Uwaga Kurtka",
        13 => "Uwaga",
        <= 15 => "Super",
        <= 20 => "Ok",
        <= 28 => "Gorąco",
        >= 30 => "Upał",
        _ => "Nie wiem"
    };
}

Zauważ, że "_" jest symbole specjalnym i określa wszystkie inne przypadki. Do łączenia warunków korzystam z "and". Aby określi, że dany kod ma się wykonać dla dwóch warunków to korzystam z "or".

Jakbyś chciał zrobić negację warunku to zawsze możesz skorzystać z "not"

Poza tym, jak piszesz takie warunki to łatwo się pomylić. Pattern Matching pilnuje Cię i zaznaczy czy dany warunek jest, aż zbyt obszerny i nie blokuje następnych.

pattermatching-01.png

Pora na kolejny przykład. Mieliśmy temperaturę, teraz czas na Pizzę. 

Pizza ma właściwość określająca jej nazwę i fakty takie jak czy mają one mięsa lub ananasa.

public class Pizza
{
    public string Name { get; set; }
                    
    public bool HasMeat { get; set; }
                    
    public bool HasAnanas { get; set; }
                    
    public void Deconstruct(out string name, out bool hasMeat, out bool hasAnanas)
    {
        name = Name;
        hasMeat = HasMeat;
        hasAnanas = HasAnanas;
    }
}

Do klasy także dodałem Dekonstruktor. Jest to specjalna metoda, która pozwoli mi rozbić instancje danej klasy na poszczególne zmienne. Przydaje się to także do Pattern Matching.

Pizza także będzie miała swój opis i uzupełnimy go korzystając z Pattern Matching.

public string Desc { get; set; }

Podobnie jak wcześniej tworzę metodę, która generuje mi losowe obiekty.

public static List<Pizza> CreateExample()
{
    var rng = new Random();
                
    var exampleCollection = Enumerable.Range(1, 14).Select
        (index => new Pizza
        {
            Name = "P" + index.ToString() + rng.Next(1, 10).ToString(),
            HasMeat = rng.NextDouble() >= 0.5,
            HasAnanas = rng.NextDouble() >= 0.5
        }
    ).ToList();
                
    return exampleCollection;
}

Dekonstruktor pozowała na szybkie przypisanie zmiennych w taki sposób. 

Pizza a = new Pizza() { HasAnanas = false, HasMeat = true, Name = "Marinara" };
Pizza b = new Pizza() { HasAnanas = false, HasMeat = true, Name = "Peperoni" };
            
var (name, hasananas, hasmeat) = a;
var (name2, hasananas2, hasmeat2) = b;

W JavScript mówi się na to operator "spread". Możesz kojarzysz temat o to przykład:

function mojaFunkcja(x, y, z) { }
var args = [0, 1, 2];
mojaFunkcja(...args);

Mając Dekonstruktor mogę warunki napisać w taki sposób.

var exampleList = CreateExample();

Pizza a = new Pizza() { HasAnanas = false, HasMeat = true, Name = "Marinara" };
Pizza b = new Pizza() { HasAnanas = false, HasMeat = true, Name = "Peperoni" };
exampleList.Add(b); exampleList.Add(a);
            
foreach (var pizza in exampleList)
{
    pizza.Desc = pizza switch
    {
        ("1", true, false) => "Taką zjem",
        ("2", true, true) => "Ujdzie",
        ("Peperoni", _, _) => "Moja ulubiona",
        ("Marinara", _, _) => "Często jadam",
        (_, _, true) => "Tej nie zjem",
        _ => "Nie wiem"
    };
}

Używając discarda "_" mogę nawet pomimo parametry, które mnie nie interesują. Może w danym warunku interesuje mnie tylko nazwa Pizzy, a pozostałe parametry są dla mnie nieważne.

Dodatkowo mogę przypisać zmienną w warunku i później z niej skorzystać.

foreach (var pizza in exampleList)
{
    pizza.Desc = pizza switch
    {
        ("2", _, var czymaAnans) => $"Pizza z Ananasem : {czymaAnans}",
        { HasMeat : true} => "Ma mieso"
    };
}

Mogę też odwołać się do konkretnej właściwości, jeśli nie pamiętam kolejności ich dekonstrukcji w danej klasie.

foreach (var pizza in exampleList)
{
    pizza.Desc = pizza switch
    {
        ("2", _, var czymaAnans) => $"Pizza z Ananasem : {czymaAnans}",
        (HasMeat: true) => $"Ma mieso",
        _ => throw new NotImlementedException()
    };
}

Dla wszystkich innych warunków zawsze mogę wyrzucić wyjątek. Od tego jest discard symbol "_".

Jeśli z jakiegoś powodu nie mogę skorzystać z dekonstrukcji to zawsze mogę utworzyć Tuplet i na jego bazie tworzyć warunki.

foreach (var pizza in exampleList)
{
    pizza.Desc = (pizza.HasMeat, pizza.HasAnanas) switch
    {
        (true, false) => "Super",
        (false, true) => "Poprostu nie",
        (false, false) => "Bez mięsa ?"
    };
}

Wzorów w jest kilka. Jakby Ciebie to interesowało o to ich fachowe nazwy.

Patterns w C# 9.0 

  • Łączące "and"
  • Lub "or"
  • Negacji "not"
  • Nawiasowy ()
  • Type (po typie)
  • Relational pattern, czyli relacyjny wzór

Właśnie jeszcze Ci nie pokazałem jak wykorzystać Pattern Matching, gdy chcemy pisać warunki operujące na typie obiektu.  Już nie będziesz musiał pisać "== typeof(coś)".

Oto kolejne klasy do przykładów. Tym razem będą to testy. Jak widzisz mamy klasę bazową Test oraz dwie klasy dziedziczące RedTest i GreenTest.

public class GreenTest : Test
{
                    
}
                    
public class RedTest : Test
{
                    
}
                    
public class Test
{
                    
}

Znowu mamy klasę generującą losowe obiekty.

public static List<Test> CreateExample()
{
    var rng = new Random();
    
    List<Test> tests = new List<Test>();
    
    for (int i = 0; i < 15 ; i++)
    {
        if (rng.Next(1,100)  % 2 == 0)
            tests.Add(new GreenTest());
        else
            tests.Add(new RedTest());
    }
    
    return tests;
}

Jeśli chcesz utworzyć warunek po typie obiektu to po prostu piszesz jego nazwę typu / klasy.

var exampleList = CreateExample();

foreach (var test in exampleList)
{
    string outcome = test switch
    {
        GreenTest => "Postive",
        RedTest => "Negative",
        _ => "unknow"
    };
    Console.WriteLine(outcome);
}

Relational pattern określa warunek, który łączy wszystkie możliwe dostępne mechanizmy.

Dodajmy do klasy bazowej Test Dekonstruktor, aby to pokazać. 

public class Test
{
    public bool IsValid { get; set; }
    public DateTimeOffset TestedOn { get; set; }
                    
    public void Deconstruct(out int minutesSinceTest, out bool isValid)
    {
        minutesSinceTest = (int)(DateTimeOffset.UtcNow - TestedOn).TotalMinutes;
        isValid = IsValid;
    }
}

public class GreenTest : Test
{
                    
}
                    
public class RedTest : Test
{
                    
}
        

Teraz mogę powiedzieć, że dla testu, który żyje dłużej niż 48 godzin i zakończył się błędem napiszę się taki napis  "

Mogę też powiedzieć, że konkretnie dla typu GreenTest, który żyje dłużej niż 24 godziny i wykonał się poprawnie wyświetlić jeszcze inny napis.

var exampleList = CreateExample();

foreach (var test in exampleList)
{
    string outcome = test switch
    {
        ( < 60 * 24 * 2, false) => "Nie poprawny test których jest młodszy niż 2 dni",
        GreenTest(> 60 * 24, true) => "Postive, ale warto go po 24 godzinach zrobić ponownie",
        _ => "unknow"
    };
    Console.WriteLine(outcome);
}

To wszystko, jeśli chodzi o Pattern Matching. To nie koniec nowości w C# 9.0/

Is not

"Is not" to alternatywny sposób pisania "!=" i sprawdzania czy dana zmienna nie ma przypisanej wartości NULL.

PirateModel m1 = new PirateModel
{ Name = "Luffy", CrewName = "StrawHats", Id = 1 };
            
PirateModel m2 = new()
{ Name = "Zorro", CrewName = "StrawHats", Id = 2 };
            
PirateModel m3 = null;

Ten kod jest poprawny

if (m3 is null)
{
            
}
            
if (m2 is not null)
{
            
}

Ten już nie ponieważ "is not" nie służy do porównań obiektów.

//if (m1 is not m2)
//{
            
//}

Dlaczego to istnieje? Szczerze to sam bym nie wiedział, ale na webinarze ktoś słusznie zwrócił uwagę, że operatory "==" i "!=" można nadpisać, a wyrażenia "is not" i "is" już nie.

Jak użyć rekordów z sensem?

Przejdźmy do rekordów. Co to jest? Czyżby C# potrzebował czegoś innego niż struktury i klasy? Po co one są? Czy to tylko krótszy sposób pisania klasy? 

Moje pierwsze spojrzenie na rekordy było zdecydowanie negatywne, Patrzysz na taki kod definicji rekordu i zapewne ty masz mnóstwo pytań.

public record Record1(int  FirstNumber, int SecondNumber);

//Czy Rekordy są typem wartościowym, 
//Czy typem referencyjnym?

Po pierwsze czy rekord jest typem wartościowym, czy referencyjnym? Mógłbyś przeczytać dokumentację, ale możesz też to sprawdzić pisząc taki kod.

Record1 rA = new(1, 2);
Record1 rAtest = rA;
Console.WriteLine(ReferenceEquals(rA, rAtest));

//true

Referencje w wyniku przyrównania się zgadzają, czyli rekordy są typem referencyjnym. 

Czy rekord to krótszy styl pisania klasy?

//Czy rekord jako klasa wygląda tak

public class Record2
{
    public string FirstNumber { get; set; }
                    
    public string SecondNumber { get; set; }
}

Istnieją duże różnice pomiędzy rekordem i klasą i nawet ten kod poniżej nie pokazuje wszystkiego.

//Podobne do tej klasy, ale to jednak nie to samo
//o tym za chwilę
public class Record3
{
    public string FirstNumber { get; init; }
                    
    public string SecondNumber { get; init; }
                    
    public Record3(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

Brakuje to jeszcze Dekonstruktora, aby ta składnia była całkowicie poprawna...Chociaż i to jeszcze nie wszystko :)

Rekord można też deklarować bardziej rozbudowaną składnią.

//To jest poprawny rekord
public record Record4
{
    public int Number1 { get; init; }
    public int Number2 { get; init; }
}

Istnieje tutaj jeden problem. Dla rekordów wszystkie ich właściwości powinny być deklarowane tylko raz. Pamiętasz to nowe słowo kluczowe "init", które omówiliśmy wiele zdań temu.

O ile Visual Studio na razie pozwala Ci na napisanie takiego rekordu to jest to łamanie głównej filozofii ich działania.

//Tutaj jest problem z nie jasnym działem rekordu

public record Record4
{
    public int Number1 { get; set; }
    public int Number2 { get; set; }
}

Dobra, jakie są jeszcze inne różnice pomiędzy rekordem a klasą. Oto klasa, która ma takie same właściwości ja mój rekord.

public class Class3
{
    public int FirstNumber { get; init; }
                    
    public int SecondNumber { get; init; }
                    
    public Class3(int firstNumber, int secondNumber)
    {
        FirstNumber = firstNumber;
        SecondNumber = secondNumber;
    }
}

Utworze 2 rekordy i klasy z właściwościami o takich samych wartościach. Trzeci rekord i klasa będzie miała zupełnie inną wartość.

Record1 rA = new(1, 2);
Record1 rB = new(1, 2);
Record1 rC = new(100, 200);
            
Class3 classA = new(1, 2);
Class3 classB = new(1, 2);
Class3 classC = new(100, 200);

Wykonajmy na nich metodę Equals.

Console.WriteLine($"Record1 rA = new(1, 2);");
Console.WriteLine($"Record1 rB = new(1, 2);");
Console.WriteLine($"Check if they are Equal : {Equals(rA, rB)}");

Console.WriteLine("***");

Console.WriteLine($"Class3 classA = new(1, 2);");
Console.WriteLine($"Class3 classB = new(1, 2);");
Console.WriteLine($"Check if they are Equal : {Equals(classA, classB)}");

Wynik jest następujący :

rekordy w C# 9.0 Equals

Dla rekordów Equals zwrócił : True, a dla klasy mamy wartość : False

Wniosek jest jeden rekordy były porównane po wartościach właściwości. Wartości są takie same więc mamy prawdę.

Natomiast klasa jest porównywana w Equals także po referencji. Mamy dwie zmienne więc są dwie różne referencje.

Console.WriteLine($"Record1 rA = new(1, 2);");
Console.WriteLine($"Record1 rB = new(1, 2);");
        
Console.WriteLine($"Check if they are Equal : {Equals(rA, rB)}");
        
Console.WriteLine($"Check if they are ReferenceEquals :" +
$"{ ReferenceEquals(rA, rB)}");

Reference Equals jak sprawdziliśmy wcześniej zwróci fałsz. Kompilator więc magicznie nie łączy rekordów do jednej referencji, jeśli mają te same wartości.

rekordy w C# 9.0 ReferenceEquals

Co zrobi operator "==" dla rekordów?

Console.WriteLine($"Record1 rA = new(1, 2);");
Console.WriteLine($"Record1 rB = new(1, 2);");

Console.WriteLine($"Check if they are Equal : {Equals(rA, rB)}");

Console.WriteLine($"Check if they are ReferenceEquals 
: {ReferenceEquals(rA, rB)}");

Console.WriteLine($"Check if they are == : {rA == rB}");

Wykona metodę Equals i zrobi porównanie po wartościach raz jeszcze.

rekordy w C# 9.0 == operator

Istnieje jeszcze jednak różnica pomiędzy klasą a rekordem.

Na przykład możesz się zastanawiać, dlaczego ten kod dla klas jest poprawny, ale ten sam kod dla rekordów wyrzuci wyjątek dla słowników.

Problem ze słownikiem record

Jeśli wrzucisz obiekt jako "klucz" do słownika to czym tak naprawdę jest ten klucz? 

Prawda leży w metodzie "GetHasCode()". To ona tworzy klucz dla słownika. Czyli ten kod stworzył dla rekordów dwa razy ten sam klucz stąd ten wyjątek. 

Zobaczmy co metoda "GetHasCode()" zwróci:

Console.WriteLine($"Record1 rA = new(1, 2);");
Console.WriteLine($"Record1 rB = new(1, 2);");

Console.WriteLine($"Check if they are Equal : {Equals(rA, rB)}");

Console.WriteLine($"Check if they are ReferenceEquals : 
{ReferenceEquals(rA, rB)}");

Console.WriteLine($"Check if they are == : {rA == rB}");

Console.WriteLine($"Hash Code rA: {rA.GetHashCode()}");
Console.WriteLine($"Hash Code rB: {rB.GetHashCode()}");
Console.WriteLine($"Record1 rC = new(100, 200);");
Console.WriteLine($"Hash Code rC: {rC.GetHashCode()}");

Console.WriteLine($"Check if they are != : {rA != rC}");

Jak widzisz, jeśli wartości w rekordzie są takie same to metoda "GetHashCode()" zwróci to samą wartość.

GetHashCode Record

Zobaczmy dla przypomnienia jak to wygląda dla klas.

Console.WriteLine($"Record3 classA = new(1, 2);");
Console.WriteLine($"Record3 classB = new(1, 2);");

Console.WriteLine($"Check if they are Equal : {Equals(classA, classB)}");
Console.WriteLine($"Check if they are ReferenceEquals : 
{ReferenceEquals(classA, classB)}");

Console.WriteLine($"Check if they are == : {classA == classB}");

Console.WriteLine($"Hash Code rA: {classA.GetHashCode()}");
Console.WriteLine($"Hash Code rB: {classB.GetHashCode()}");
Console.WriteLine($"Record3 classC = new(100, 200);");
Console.WriteLine($"Hash Code rC: {classC.GetHashCode()}");

Console.WriteLine($"Check if they are != : {classA != classC}");

Dla klas, nawet jeśli wartości właściwość są takie same to metoda "GetHashCode()" bierze także pod uwagę adres do referencji obiektu więc wartości są różne.

Dla klas GetHasCode

Deconstructor

Dekonstruktory już mogłeś zobaczyć w użyciu w Pattern Matching. Czy jednak wiesz, że rekordy domyślnie tworzą go dla wszystkich właściwości?  Taki kod jest wiec poprawny

Record1 rA = new(1, 2);

var (a, b) = rA;

Nawet Visual Studio może zaproponować Ci małe poprawki.

Deconstructor rekordy

Visual Studio naturalnie też widzi taką metodę.

Deconstructor Visual Studio

Użycie "with" aby utworzyć nowy rekord z istniejącego

Główna filozofia Rekordów polega na tym, aby były one niezmienne (immutable). Czyli jeśli chcesz zmienić coś w rekordzie to sorry musisz utworzyć nowy rekord.

Jak jednak szybko utworzyć kopie rekordy zmieniając w nim tylko jedną właściwość. Na pomoc przychodzi słowo kluczowe "with".

Record1 rA = new(1, 2);
Record1 rAtest = rA;
Console.WriteLine(ReferenceEquals(rA, rAtest));
            
Record1 newRa = rA with
{
    SecondNumber = 200
};

Teraz pora odpowiedź szybko na masę innych pytań związanymi z rekordami

Czy można utworzyć rekordy generyczne ? Tak

public record Data<T>(int id, T Data);

Czy rekordy mogą implementować interfejs ? Tak

public interface IProduct<T>
{
   T Data { get; }
}

public record Game<T> : Audit ,IProduct<T>
{
    public T Data { get; init; }
}

Rekordy nie mogą dziedziczyć po klasach

dziedziczenie rekordów

Rekordy mogę dziedziczyć po rekordach.

dziedziczenie rekordów gdy rekord

Czy rekordy mogą być partial ? Tak

public partial record Data(string Name);

Czy rekordy mogą użyć atrybutów? Tak, ale składnia jest średnio czytelna. Jak widzisz atrybut mogę dodać nawet to pól.

public record Data([property:JsonPropertyName("dataName")] string Name, 
   [property:JsonPropertyName("dataCity")]string City);


public record Data([field:Foo("field")][property:Foo("prop")] string Name);

oto przykład dodania swojego atrybutu.

[AttributeUsage(AttributeTargets.All)]
class MyAttribute : Attribute
{
   public MyAttribute(string _) { }
}
 
public record Data([My("param")][field:My("field")][property:My("prop")] string Name);

W takich wpadkach bardziej rozbudowana definicja rekordu na pewno jest czytelniejsza.

public record Numbers
{
    [JsonPropertyName("One")]
    public string First { get; init; }
                    
    [JsonPropertyName("Two")]
    public string Second { get; init; }
}

Pamiętaj jednak aby wszystkie właściwości rekordów były tylko do inicjalizacji. 

//To jest tutaj problem z nie jasnym działem rekordu

public record Record4
{
    public int Number1 { get; set; }
    public int Number2 { get; set; }
}

Czy można dodać metod i właściwości do rekordów ? Oczywiście, że można. Możesz nawet w ten sposób nadpisać poziom dostępność danej właściwości.

public record Record5(int FirstNumber, int SecondNumber);

public record Record6(int FirstNumber, int SecondNumber)
{
    internal int SecondNumber { get; init; } = SecondNumber;
}

Z ciekawości możesz zobaczyć jak działa metoda "ToString()" dla rekordów. Właściwości niepubliczne nie są dodawane do metody "ToString()".

Metoda ToString()

Oto przykład dodania właściwości do rekordu.

public record Record7(int FirstNumber, int SecondNumber)
{
    internal int SecondNumber { get; init; } = SecondNumber;
                    
    public string JavaScriptLikeSumOfNumbers { get => $"{FirstNumber}{SecondNumber}"}
}

Tutaj natomiast dodaliśmy metodę, która zwraca dwie wartości spakowane jako Tuple.

public record Record8(int FirstNumber, int SecondNumber)
{
    public (int,int) ReturnHasCodes()
    {
        return (FirstNumber.GetHashCode(), SecondNumber.GetHashCode());
    }
}

A tutaj dla twojej ciekawości wyniki metody "ToString()".

Metoda ToString()

Jeśli chodzi o dziedziczenie rekordów to pamiętaj, że właściwości muszą być uzupełnione w każdym konstruktorze.

Dziedziczenie rekordów

Dokument nie definicji "IsDeleted" i "wasUpdated" dlatego dziedziczenie nie może zajść.

Zalety Rekordów?

Po co są rekordy? No cóż, przynajmniej ustaliśmy, że nie są one inaczej zapisaną klasą.

Oto ich zalety :

  • Krótsza składnia
  • Bezpieczne wielowątkowo, ponieważ są one niezmienne (immutable)
  • Łatwe są w użyciu
  • Posiadają domyślny dekonstuktor

Kiedy ich użyć :

  • Gdy masz dane, które wiesz, że mają się nie zmieniać
  • Do wywołania API
  • Do przetwarzania danych
  • Gdy masz dane tylko do odczytu
  • Gdy operujesz na wielu-wątkach

Kiedy ich nie użyć:

  • Kiedy musisz zmienić dane

Inne zmiany : Natywne Int

Zmiany w C# 9.0 na tym się nie kończą. Chociaż musiałem bardziej pogrzebać w internecie aby je znaleźć.

Jak wiesz .NET 5 może działać na Linux i MAC. W tym celu dla nich powstała obsługa ich natywnych liczb całkowitych.

nint nativeInt = 55;

Console.WriteLine(nint.MaxValue);

Compiled in x86 Configuration
Output: 2147483647
Compiled in x64 Configuration
Output: 9223372036854775807

Inne zmiany : Discard w Lambda

Discard może być wykorzystany w wyrażeniu lambda

Func<int,int,int> zero = (_,_) => 0;

Inne zmiany : partial w metodzie

Jeśli klasa jest partial to metoda też może być partial.

partial class Doing
{
    internal partial bool DoSomething(string s, out int i);
}

partial class Doing
{
    internal partial bool DoSomething(string s, out int i)
    {
        i = 0;
        return true;
    }
}

Inne zmiany : Konwariancyjne typy zwracane

Ta zmiana akurat rozwaliła mi głowę. Pomyśl, że masz dwie klasy Cat i Tigger. Klasa Cat ma metodę GetCat().

Przed C# 9.0 musiałeś nadpisywać metodę bazową w taki sposób.

public virtual Cat GetCat()
{
    // This is the parent (or base) class.
    return new Cat();
}

public override Cat GetCat
{
    //You can return the child class, but still return a Cat
    return new Tigger();
}

Teraz w C# 9.0 możesz jawnie powiedzieć, ,że zwrócisz dziedziczącą klasę w takiej bazowej metodzie.

public virtual Cat GetCat()
{
    // This is the parent (or base) class.
    return new Cat();
}

public override Tiger GetCat()
{
    return new Tigger();
}

Żyjemy w ciekawych czasach.

Inne zmiany : GetEnumerator jako extension method

Metoda GetEnumerator<T> daje możliwość wykonywania pętli foreach na danym typie.

Teraz metoda GetEnumerator<T> może być metodą rozszerzeniową.

public static class Extensions
{
    public static IEnumerator<T> GetEnumerator<T> (this IEnumerator<T> enumerator) =>
}

Możesz więc teraz napisać swoją interpretacje pętli foreach dla typów zamkniętych jak : int, bool, byte czy enum.

Link do webinaru na ten temat masz tutaj : https://www.youtube.com/watch?v=ATbLEyd_1Kg