Baby it's 8C# 8.0 przyniósł ze sobą wielkie zmiany. Niektóre z nich nie są zmianami kosmetycznymi i wywracają pewne pytania rekrutacyjne na Juniora C# na opak. Dobra wiadomość jest taka, że .NET Core 3.1 i .NET Standard 2.1 korzysta już domyślnie z C# 8.0 i nie trzeba już dodawać dziwnych wpisów preview.

W innym wpisie omówiłem już obsługę wartości Null w typach referecyjnych więc nie będę robił tego ponownie.  

Zobaczmy co innego przyniósł ze sobą C# 8.0.

ReadOnly i struktury

Struktura jest rzadko przez nas używana daje jednak ona pewne korzyści związane z użyciem pamieci. Wiedziałeś jednak, że strukturach za każdym razem, gdy wykonujesz metodę w niej silnik C# tworzy dla bezpieczeństwa kopię całej struktury.

To nie brzmi jak dobra optymalizacja, zwłaszcza jeśli twoja metoda w strukturze niczego nie zmienia wewnątrz niej. O tej pory w C# 8 możesz oznaczyć takie metody jako metody readonly.

public struct Cube
{
    public double Length { get; set; }

    public double Height { get; set; }

    public double Width { get; set; }

    //nie musisz robić kopi bezpieczeństwa
    public readonly double Area()
    {
        return Length * Height * Width;
    }
}

Gdy będziesz próbował złamać swój własny nakaz i zmienić zawartość struktury to dostaniesz błąd.

W sumie to też dlatego, że kompilator wewnątrz tej metody traktuje te automatyczne właściwości jak pola tylko do odczytu.

Cannont assign to beacuse it is read only

Jeśli korzystasz z właściwość, która korzysta z pola prywatnego to w takim wypadku dostaniesz ostrzeżenie, że kopia twojej struktury i tak zostanie wykonana w trakcie wywołania metody readonly.

właściwości z polami prywatnymi

Domyślne metody dla interfejsów

Pora na łamanie wielkich zasad języka C#. Jak wiesz w interfejsach nie można było do tej pory tworzyć definicji swoich metod. Interfejs też nie mógł mieć swoich pól statycznych.

Od tego była klasa abstrakcyjna . Na rozmowach kwalifikacyjnych często ludzie pytają, jaka jest różnica pomiędzy interfejsem a klasą abstrakcyjną, a teraz w C# 8.0 można by powiedzieć, że tej różnicy prawie nie ma żadnej.

Swoją drogą skoro w interfejsach można deklarować metody czy to oznacza, że w C# 8.0 mamy prymitywne, ale możliwe wielodziedziczenie. Wychodzi na to, że tak

Po co to wszystko jest? Z taką wielką mocą przychodzi wielka odpowiedzialność.

Wyobraź sobie, że masz następujące interfejsy i klasy

public interface IStock
{
    void Calculate();
}

public class CDProjectRedStock : IStock
{
    public void Calculate()
    {
        Console.WriteLine("Calc");
    }
}

Wszystko wygląd pięknie, ale co się dzieje, gdy chcesz dodać nową metodę do interfejsu, ale na chwilę obecną tę metodę może implementować tylko jedna klasa z całego zbioru.

Wiele interfejsów

Co robisz?

Wyrzucasz wtedy wyjątek NotImplementException?

Rozbijasz interfejs na mniejsze według zasady Interface Separation. Jest to w końcu jedna z zasad z S.O.L.I.D więc wydaje się, że to dobry kierunek.

public interface IStock
{
    void Calculate();
}

public interface ISubTotal
{
    void CalculateSubTotal();
}

public class CDProjectRedStock : IStock, ISubTotal
{
    public void Calculate()
    {
        Console.WriteLine("Calc");
    }

    public void CalculateSubTotal()
    {
        Console.WriteLine("New Calc Sub");
    }
}

public class BankPolskiStock : IStock
{
    public void Calculate()
    {
        Console.WriteLine("Calc");
    }
}

A co by było, gdybyś mogli rozwiązać ten problem trzecim sposobem

Czyli dodać metodę do samego interfejsu, która obsłuży domyślnie wszystkie inne klasy, które nie chcą tej metody z tego interfejsu implementować.

public interface IStock
{
    void Calculate();

    //defaults
    void CalculateSubTotal()
    {
        Console.WriteLine("Calc Sub");
    }
}

public class CDProjectRedStock : IStock
{
    public void Calculate()
    {
        Console.WriteLine("Calc");
    }

    public void CalculateSubTotal()
    {
        Console.WriteLine("New Calc Sub");
    }
}

public class BankPolskiStock : IStock
{
    public void Calculate()
    {
        Console.WriteLine("Calc");
    }
}

Jeśli ufasz sobie może to tymczasowo być lepszym rozwiazaniem niż rozbijanie interfejsu. 

Dodatkowo ten mechanizm czuje, że powstał po to ,aby w frameworku jak Xamarin była możliwość obsługi domyślnych metod w interfejsach. Może programujesz na różne urządzenia jak Android czy iOS, ale pewien kod się powtarza.

Możesz tworzyć pola statyczne do interfejsów, ale to brzmi jak bardzo zły pomysł. Pola statyczne w interfejsach są tylko do użycia w nich samych...i lepiej się trzymać takie zasady projektowej.

public interface IStock
{
    public static void SetDefaultName(string n)
    {
        defaultName = n;
    }

    private static string defaultName = "Stock";

    void Calculate();

    //defaults
    void CalculateSubTotal()
    {
        Console.WriteLine(defaultName);
    }
}

Wyrażenia Switch

Wyrażenie Switch będzie można napisać dużo krócej. Jeśli nie masz skomplikowanej logiki w tym wyrażeniu to, zamiast pisać to:

public static class SwitchExpressions
{
    public static double DoMath(double x, double y, MathType mathType)
    {
        double result = 0;

        switch (mathType)
        {
            case MathType.Add:
                result = x + y;
                break;
            case MathType.Substract:
                result = x - y;
                break;
            case MathType.Multiply:
                result = x * y;
                break;
            case MathType.Divide:
                result = x / y;
                break;
            default:
                throw new Exception("Passed MathType is wrong")
        }

        return result;
    }
}

public enum MathType
{
    Add,
    Substract,
    Multiply,
    Divide
}

Możesz napisać to:

public static class SwitchExpressions
{
    public static double DoMath(double x, double y, MathType mathType)
    {
        double result = 0;

        result = mathType switch
        {
            MathType.Add => x + y,
            MathType.Divide => x / y,
            MathType.Multiply => x * y,
            MathType.Substract => x - y,
            _ => throw new Exception("Bad");
        };

        return result;
    }
}

Kreska _ to symbol określający wszystkie inne przypadki. W tym wypadku dla wszystkich innych przypadków chce wyrzucić wyjątek. 

Oczywiście pojawia się problem. Jeśli chcesz obsługi kilku możliwych warunków case w switch to musisz wrócić do starej deklaracji.

switch (mathType)
{
    case MathType.Add:
    case MathType.Substract:
        result = x - y;
        break;

5: Deklaracje Using

Pewnym osobom zapewne nie podobał się fakt, że z każdym usingiem kod jest coraz bardziej zaklarmowany w tych nawiasach. 

Kod Using istnieje po to, aby powiedzieć C# kiedy dany zasób ma zostać zlikwidowany. Czy można to zrobić lepiej bez odwoływania się bezpośrednio do metody dispose?

W C# 8.0 odpowiedź brzmi tak:

public static class TransformFile
{
    public static int Convert()
    {
        int output = 0;

        using (var inputFile = new StreamReader(@"D:\test.txt"))
        {
            using (var outputFile = new StreamWriter(@"D:\output.txt"))
            {
                string line;

                while ((line = inputFile.ReadLine()) != null)
                {
                    outputFile.WriteLine("Koleś mówi :"+line);
                    output += 1;
                }
            }
        }

        return output;
    }
}

Teraz możesz to tak zadeklarować 

using var inputFile = new StreamReader(@"D:\test.txt");

Co sprawia, że cały kod teraz będzie czytelniejszy.

public static class TransformFile
{
    public static int Convert()
    {
        int output = 0;


        using var inputFile = new StreamReader(@"D:\test.txt");

        using var outputFile = new StreamWriter(@"D:\output.txt");

        string line;

        while ((line = inputFile.ReadLine()) != null)
        {
            outputFile.WriteLine("Koleś mówi :" + line);
            output += 1;
        }

        return output;
    }
}

Kiedy jednak te zasoby plikowe zginą? Wraz ze skończeniem metody dla obu zmiennych wykona się metoda dispose i zasób zostanie zwolniony z pamięci.

Wskaźniki i zakres tablicy w pętli foreach

W C# 8 masz teraz większą kontrolę nad powiedzeniem, jaki konkretny element z tablicy chcesz wyświetlić.

Korzystając z ^1 i ^2 może łatwo wyświetlić ostatni i przedostatni element w tablicy.

var languages = new string[]
{
    "C#","Java","Python","Swift","C++","Objective-C","SQL"
};
// languages[languages.Length - 1] = "SQL"
// languages[languages.Length - 2] = "Objective-C"

Console.WriteLine($"Ostatnie element listy to {languages[^1]}");
Console.WriteLine($"Przed ostatni element listy to {languages[^2]}");

Gdybyś chciał wyświetlić element od drugiego indeksu, czyli 3 elementu tablicy do 5 elementu (na indeksie 4) to musisz taki kod napisać.

Console.WriteLine($"Pokaż mi elementy od 2 indeksu (3 element) " +
    $"do 4 indeksu");

foreach (var item in languages[2..5])
{
    Console.WriteLine(item);
}

Jeżeli chcesz wyświetlić wszystkie element oprócz ostatniego to taki kod piszesz.

Console.WriteLine($"Pokaż mi elementy oprócz ostatniego");
foreach (var item in languages[..^1])
{
    Console.WriteLine(item);
}

Jeżeli chcesz pokazać wszystkie element oprócz pierwszego to piszesz taki kod

Console.WriteLine($"Pokaż mi elementy oprócz pierwszego");
foreach (var item in languages[1..])
{
    Console.WriteLine(item);
}

6 : Null Coalescing Assignment

Do C# 8.0 doszedł też nowy operator ??=. Jego zadaniem jest zadeklarować daną zmienną, jeżeli jest ona niezadeklarowana, czyli ma NULL.

public class LotteryNumbers
{
    public List<int> Numbers { get; set;}
    public DateTime When { get; set; }
    public bool IsThereAWinner { get; set; }
}

public class AnotherClass
{
    public void Show()
    {
        LotteryNumbers lotteryNumbers = new LotteryNumbers();

        if (lotteryNumbers.Numbers == null)
        {

        }

        foreach (var item in lotteryNumbers.Numbers)
        {
            Console.WriteLine(item);
        }
    }
}

Zamiast IF-ów możesz użyć operatora, gdy nie masz pewności czy dana lista został już zadeklarowana.

public void Show()
{
    LotteryNumbers lotteryNumbers = new LotteryNumbers();

    lotteryNumbers.Numbers ??= new List<int>();

    foreach (var item in lotteryNumbers.Numbers)
    {
        Console.WriteLine(item);
    }
}

C# z każdą wersją wygląda coraz lepiej . Jestem ciekaw co wymyślą w .NET 5.0 i w C# 9.0