# Pobieżne wprowadzenie do podstaw części C#, Tom I, Część I, Dodatek I – C# 8

v1.0.0 Copyright © Mateusz Gienieczko 2019

0. Plan

  1. Plan
  2. Nullable reference types
  3. IAsyncEnumerable/await foreach oraz IAsyncDisposable/await using
  4. Default interface implementations
  5. switch i pattern matching
  6. Index i Range
  7. Cieszmy się z małych rzeczy

1. Nullable reference types

The billion dollar mistake fixed! Well, kinda.

1.1 The theory

Dodajemy magiczną linijkę do .csproj

<Nullable>enable</Nullable>

I dostajemy masę przydatnych warningów:

Duck duck = null;
warning CS8600: Converting null literal or possible null value to non-nullable type.

Można pójść na całego i traktować te warningi jako errory.

<WarningsAsErrors>CS8600;CS8601;CS8602;CS8603</WarningsAsErrors>

Jest ich Podobnie jest z niezainicjalizowanymi polami i autoimplemented properties

class Duck 
{ 
    string Name { get; } 
    public Duck() { } 
}
warning CS8618: Non-nullable property 'Name' is uninitialized. Consider declaring the property as nullable.

1.2 Obey me, dammit!

Czasami wiemy, że jakiś nullable jest tak naprawdę not-null albo nie przejmujemy się tym co się stanie, jeśli w środku będzie siedział null. Możemy wtedy użyć Nullable-Forgiving Operator !, znany też pod bardziej wdzięczną nazwą Dammit! Operator (ew. Bang! Operator). Jest to oficjalna nazwa wymieniona w dokumentacji MSDN-a i nikt nigdy nie powinien używać innej.

interface ISqueakingMechanism 
{ 
    void Squeak(); 
} 

class Duck 
{ 
    string Name { get; } 
    ISqueakingMechanism? SqueakingMechanism { get; } 

    ...
} 

void SqueakMeBaby(ISqueakingMechanism squeakingMechanism) 
{ 
    if (squeakingMechanism == null) 
    { 
        throw new ArgumentNullException(nameof(squeakingMechanism)); 
    } 
} 

void SqueakADuck(Duck duck) => SqueakMeBaby(duck?.SqueakingMechanism!);

1.3 The practice

Nullable reference types to niestety jedynie obietnice palcem na piasku pisane. Na poziomie CLR-a nie ma różnicy między object a object?, nie jest on wrappowany w Nullable<T> tak jak w przypadku nullable value types. W związku z tym nikt nikomu nie zabroni wrzucić null a gdzie nie trzeba i nadal należy programować defensywnie, w szczególności biblioteki.

1.4 Kiedy używać?

Yyy, zawsze.

1.5 Kiedy nie używać?

Czego nie zrozumieliście. Dosłownie nie ma powodu żeby tego nie włączyć w nowych projektach.

1.6 A generyki?

class Duck<T>
{
    T Name { get; }
    public Duck(T name) => Name = name;
}

Z generykami jest problem, bo w Duck<T> T jest unconstrained generic parametrem. Intuicyjnie powinno tam pasować wszystko, przed C#8 mogliśmy mieć zarówno Duck<object> jak i Duck<int> jak i Duck<int?>, więc sensownie by było, jakby teraz dało się wsadzić w T również object?. Mamy więc specjalny constraint notnull oraz nowe class? i struct?.

Duck<T> // Unconstrained, T jest czymkolwiek.
Duck<T> where T : notnull // T jest albo non-nullable reference type
                          // albo non-nullable value type.
Duck<T> where T : class  // T jest non-nullable reference type.
Duck<T> where T : class? // T jest reference type.

Problemów z generykami jest więcej, dlatego mamy wachlarz atrybutów takich jak [MaybeNull, [NotNull] itp. Wszystko to bardzo skomplikowane więc zapraszam tutaj.

2. IAsyncEnumerable/await foreach oraz IAsyncDisposable/await using

Kolejny feature, o którym za rok będziemy myśleć “no oczywiście, że C# to ma, it makes so much sense!”.

2.1 Streaming

Web rules the world and web is asynchronous. Pobieranie wartości z chmury albo jakiegoś API z włączonym pagingiem jest z natury asynchroniczne. Dlatego mamy IAsyncEnumerable<out T>, które zwraca IAsyncEnumerator<out T>

interface IAsyncEnumerator<out T> : IAsyncDisposable
{
    T Current { get; }
    ValueTask<bool> MoveNextAsync();
    ValueTask DisposeAsync();
}

Pozwala to robić różne niesamowite rzeczy, np. asynchroniczne iterator blocki.

async IAsyncEnumerable<Duck> GetDucksAsync()
{
    var page = 1;
    List<Duck> ducks;
    do {
        ducks = await _externalApi.GetDucksPageAsync(page);
        foreach (var duck in ducks)
        {
            yield return duck;
        }
    } while (ducks.Any());
}

Iterujemy się używając konstrukcji await foreach, która również jest pattern based, jak zwykły foreach.

async static Task Main()
{
    await foreach (var duck in GetDucksAsync())
    {
        duck.Squeak();
    }
}

2.2 But can it LINQ?

Of course it can. Wystarczy pobrać paczkę System.Linq.Async.

2.3 Dis… wait for it… pose!

Czasami zwolnienie zasobu może wymagać jakiejś czasochłonnej operacji, np. poczekania na zakończenie połączenia. Mamy więc IAsyncDisposable i await using.

class DuckThatSqueaksTenTimesEvenIfItDies : IAsyncDisposable
{
    private volatile bool _isDying = false;
    private readonly Task _squeakingTask;
    
    public DuckThatSqueaksTenTimesEvenIfItDies() => 
        _squeakingTask = SqueakTenTimesAsync();

    public async ValueTask DisposeAsync()
    {
        _isDying = true;
        await _squeakingTask;
    }

    private async Task SqueakTenTimesAsync()
    {
        for (var i = 0; i < 10; ++i)
        {
            await Task.Delay(1000);
            var message = _isDying ? "SQUEAAAK!!!" : "Squeak!";
            Console.WriteLine(message);
        }
    }
}
public static async Task Main()
{
    await using (var duck = new DuckThatSqueaksTenTimesEvenIfItDies())
    {
        await Task.Delay(7500);
        Console.WriteLine("Existence is dormant.");
    }
}
> Squeak!
> Squeak!
> Squeak!
> Squeak!
> Squeak!
> Squeak!
> Squeak!
> Existence is dormant.
> SQUEAAAK!!!
> SQUEAAAK!!!
> SQUEAAAK!!!

3. Default interface implementations

Time for the elephant in the room.

3.1 Disclaimer

Ten feature jest trochę dziwny. .NET community jeszcze nie doszło do konsensusu na temat tego czy to dobrze, czy może niedobrze, że to jest. Ja jednak jestem tylko biednym skrybą, więc nie ma dobrze albo niedobrze i dutifully opiszę wszystko co warte opisu na ten temat.

3.2 Rationale

Idea za tym ficzerem jest szczytna. Załóżmy, że mamy interfejs kaczki.

public interface IDuck
{
    string Name { get; }
    Color Color { get; }
}

public class Duck : IDuck 
{
    public string Name { get; }
    public Color Color { get; }
}

Wypuściliśmy ten interfejs jako API, naszą bibliotekę pobrały setki ludzi i dopiero teraz dostajemy requesty pod tytułem “well I think a duck should squeak so maybe add it to the interface?”.
Problem jest taki, że jeśli napiszemy tak:

public interface IDuck
{
    string Name { get; }
    Color Color { get; }

    void Squeak();
}

To popsuliśmy wszystkie istniejące implementacje IDuck. Ich kod już się nie skompiluje. A może część ludzi ma wyjebane w to, że kaczka robi squeak i chcieliby pobrać nową wersję naszej biblioteki, ale niekoniecznie chcieliby implementować jakąś niepotrzebną metodę. Well, since C#8 we can say:

public interface IDuck
{
    string Name { get; }
    Color Color { get; }

    public void Squeak() => Console.WriteLine("Squeak!");
}
public class Duck : IDuck 
{
    public string Name { get; }
    public Color Color { get; }
}

I uwaga czary mary

IDuck duck = new Duck();
duck.Squeak();
> Squeak!

Szok, niedowierzanie. Jak ktoś chce sobie zaimplementować własne squeak to nadal może.

class Duck : IDuck
{
    public string Name { get; }
    public Color Color { get; }

    public void Squeak() => Console.WriteLine("Better squeak!");
}

IDuck duck = new Duck();
duck.Squeak();
> Better squeak!

Działa to podobnie jak explicit interface implementations, czyli metoda jest widoczna tylko w interfejsie. Czyli to:

class Duck : IDuck
{
    public string Name { get; }
    public Color Color { get; }
}

Duck duck = new Duck();
duck.Squeak();

się nie kompiluje, bo Duck nie ma metody Squeak.

Jako część tego ficzera dodano access specifiery do interfejsów (domyślnie nadal jest publiczny).

3.3 Usage guidelines

Granica między interfejsami a klasami abstrakcyjnymi się przez ten feature zaciera. Kiedy tego używać? Jakie są ryzyka? Honest answer is – I don’t know. Tak jak wspominałem, community jeszcze jest on the fence about this i zajmuje się tykaniem tego patykiem i psuciem tego ile wlezie. Jak ktoś jest zainteresowany tematem to zapraszam do własnego researchu, bo tym razem (szok) (niedowierzanie) nie jestem ekspertem.

4. switch i pattern matching

Let’s talk about something more joyful: functional programming!

4.1 Co to jest?

Patterny to coś czego można użyć razem z pattern expression. Przed C#8 jedynym pattern expression był is. Składnia isa to cuś is pattern. Wszystkie wymienione patterny można stosować z is tak samo jak ze switch expression, ale to switch expression będziemy używać w przykładach.

4.2 Switch expressions

Zaczynając od najprostszego przykładu, mamy switcha:

public int ColorValue(Color color)
{
    switch (color)
    {
        case Color.Yellow:
            return 42;
        case Color.Green:
            return 420;
        default:
            return 0;    
    }
}

Zmieniamy to na switch expression:

public int ColorValue(Color color)
{
    return color switch
    {
        Color.Yellow => 42,
        Color.Green => 420,
        _ => 0
    };
}

Albo ładniej:

public int ColorValue(Color color) => color switch
{
    Color.Yellow => 42,
    Color.Green => 420,
    _ => 0
};

No i super. Podłoga to specjalny discard, który matchuje cokolwiek. Teraz bazując na tych klasach:

interface IDuck
{
    string Name { get; }
    string Color { get; }
    void Squeak();
}

class Duck : IDuck
{
    public string Name { get; }
    public Color Color { get; }

    public void Squeak() => Console.WriteLine("Squeak!");
}

class BetterDuck : IDuck
{
    public string Name { get; }
    public Color Color { get; }

    public void Squeak() => Console.WriteLine("Better squeak!");
}

będziemy sobie dodawać kolejne patterny dostępne w C#8.

4.3 Type pattern

Type pattern to to czego używamy najczęściej przy is. Matchuje wtedy, kiedy rzecz jest danego typu.

string GetName(IDuck duck) => duck switch
{
    Duck _ => duck.Name,
    BetterDuck _ => $"{duck.Name} (but better)",
    _ => throw new ArgumentException("The duck needs a name.", nameof(duck));
};

Bonusowo możemy sobie zadeklarować zmienną danego typu po zmatchowaniu (stąd te discardy powyżej):

void SqueakADuck(object @object) => @object switch
{
    Duck duck => duck.Squeak(),
    _ => throw new ArgumentException("You call that a duck?", nameof(@object));
}

4.4 Sidenote: when

Klauzuli when można używać w switch expression tak samo jak w zwykłych.

string GetName(IDuck duck) => duck switch
{
    BetterDuck _ => $"{duck.Name} (but better)",
    Duck _ when duck.Name == "Jacuś" => "Jacuuuuś!",
    Duck _ => duck.Name,
    _ => throw new ArgumentException("The duck needs a name.", nameof(duck))
};

4.5 var pattern

Ten pattern matchuje wszystko i deklaruje zmienną o danej nazwie.

anything is var varname

Na pierwszy rzut oka wygląda bezużytecznie, ale za chwilę się okaże, że jednak nie.

4.6 Property pattern

Tutaj dochodzimy do prawdziwego pattern matchingu. Oto metoda, która matchuje dowolne żółte i zielone kaczki:

string ColorDescription(IDuck duck) => duck switch
{
    { Color: Color.Yellow } => "Nice yellow.",
    { Color: Color.Green } => "Calming green.",
    _ => "Some other boring color."
};

Możemy to też połączyć z type pattern:

string ColorDescription(IDuck duck) => duck switch
{
    BetterDuck { Color: Color.Yellow } => "Better yellow.",
    BetterDuck { Color: Color.Green } => "Better green.",
    Duck { Color: Color.Yellow } => "Nice yellow.",
    Duck { Color: Color.Green } => "Calming green.",
    _ => "Some other boring color."
};

Mimo że pattern nazywa się “property pattern”, to matchuje też publiczne pola, ale grzeczni programiści nie używają publicznych pól i tak.

Mamy też pattern { } oraz null, które matchują odpowiednio cokolwiek not-null i null.

string ColorDescription(IDuck? duck) => duck switch
{
    { Color: Color.Yellow } => "Nice yellow.",
    { Color: Color.Green } => "Calming green.",
    { } => "Some other boring color.",
    null => "The cold empty black of the Void"
};

I teraz twist fabuły, to po dwukropkach w property pattern to też pattern, więc można je zagnieżdżać.

class DuckMatrioshka : IDuck
{
    public string Name { get; } = "Jacuś";
    public Color Color { get; } = Color.Yellow;
    public IDuck Nested { get; }

    public void Squeak()
    {
        Nested.Squeak();
        Console.WriteLine("Squeak!");
    }

    public DuckMatrioshka(IDuck nested) => Nested = nested;
}
string GetNestingLevel(IDuck? duck) => duck switch
{
    DuckMatrioshka
    {
        Nested: var d
    } when !(d is DuckMatrioshka) => "Two.",
    DuckMatrioshka
    {
        Nested: DuckMatrioshka
        {
            Nested: var d
        }
    } when !(d is DuckMatrioshka) => "Three!",
    DuckMatrioshka 
    {
        Nested: DuckMatrioshka
        {
            Nested: DuckMatrioshka
            {
                Nested: var d
            }
        }
    } when !(d is DuckMatrioshka) => "FOUR!",
    DuckMatrioshka _ => "Ducks within ducks within ducks...",
    { } => "One.",
    _ => "Zero."
};

IDuck? zero = null;
var one = new Duck("Jacuś", Color.Yellow);
var two = new DuckMatrioshka(one);
var three = new DuckMatrioshka(two);
var four = new DuckMatrioshka(three);
var five = new DuckMatrioshka(four);

foreach (var duck in new[] { zero, one, two, three, four, five })
{
    Console.WriteLine(GetNestingLevel(duck));
}
> Zero.
> One.
> Two.
> Three!
> FOUR!
> Ducks within ducks within ducks...

Jak widać var pattern do czegoś się przydał.

4.7 Positional pattern

Można też matchować value tuple, a dokładniej to cokolwiek co ma dekonstruktor.

public bool AreMatchingColorsAndTypes((IDuck, IDuck) pairOfDucks) => pairOfDucks switch
{
    ({ Color: var colorOne }, { Color: var colorTwo }) when colorOne == colorTwo => true,
    _ => false
};

4.8 Konkluzje

Pattern matching zdecydowanie ułatwia przekazywanie logiki, która w innym wypadku byłaby ifologią albo dużym blokiem switch/case'ów. Można być pewnym, że w przyszłych release’ach C# dostaniemy więcej patternów do zabawy. Największym minusem samych switch expression jest to, że jedyny sposób na przełożenie konstrukcji

switch (sth)
{
    case A:
    case B:
        return 42;
}

jest

sth switch
{
    A => 42,
    B => 42
}

Nie da się nadać dwóm matchom tej samej wartości. Jeżeli C# team tego nie poprawi w 8.1 then they’re absolutely trolling.

5. Index i Range

You think we can only steal from be inspired by functional languages? Python slicing here we go.

5.1 Index

Index to indeks.

5.2 Range

Range to para indeksów.

5.3 Index from end operator

Wyrażenie ^i oznacza i-ty od końca.

5.4 Range operator

x..y to Range od indeksu x do y.

5.5 Is it really that simple?

Yes.

5.6 Przykłady

void PrintName(Duck duck) => Console.WriteLine(duck.Name);

void PrintNames(IEnumerable<Duck> ducks) =>
	Console.WriteLine(string.Join(" ", ducks.Select(d => d.Name)));

var ducks = new [] 
{                           // index from start    index from end
    new Duck("Jacuś"),      // 0                   ^5 (^ducks.Length)
    new Duck("Piotruś"),    // 1                   ^4
    new Duck("Azathoth"),   // 2                   ^3
    new Duck("Psuchawrl"),  // 3                   ^2
    new Duck("Tsathoggua")  // 4                   ^1
                            // 5 (ducks.Length)    ^0
};
PrintName(ducks[0]);
PrintName(ducks[^5]);
PrintName(ducks[^ducks.Length]);
PrintName(ducks[^1]);
// PrintName(ducks[^0]); - this would crash with IndexOutOfRangeException
> "Jacuś"
> "Jacuś"
> "Jacuś"
> "Tsathoggua"
PrintNames(ducks[0..^0]);
PrintNames(ducks[1..3]);
PrintNames(ducks[1..^2]);
PrintNames(ducks[2..2]);
PrintNames(ducks[^4..2]);
// PrintNames(ducks[3..1]); - this would crash with IndexOutOfRangeException
> Jacuś Piotruś Azathoth Psuchawrl Tsathoggua
> Piotruś Azathoth
> Piotruś Azathoth
>
> Piotruś
PrintNames(ducks[0..]);
PrintNames(ducks[1..]);
PrintNames(ducks[..1]);
PrintNames(ducks[..]);
> Jacuś Piotruś Azathoth Psuchawrl Tsathoggua
> Piotruś Azathoth Psuchawrl Tsathoggua
> Jacuś
> Jacuś Piotruś Azathoth Psuchawrl Tsathoggua

6. Cieszmy się z małych rzeczy

Tak szybko M$ je implementuje. Oto paczka małych QoL changes.

6.1 Using declarations

Zadeklarowanie zmiennej używając using declaration działa tak samo jak napisanie using blocka do końca obecnego scope’u.

void SaveDuck(Duck duck)
{
   if (duck.Color == Color.Pink)
   {
       throw new ArgumentException("We don't serve your kind here!", nameof(duck));
   }

   using var _dbContext = new DbContext(connectionString);
   _dbContext.Ducks.Add(duck);
   _dbContext.SaveChanges();
}

To to samo co:

void SaveDuck(Duck duck)
{
   if (duck.Color == Color.Pink)
   {
       throw new ArgumentException("We don't serve your kind here!", nameof(duck));
   }

   using (var _dbContext = new DbContext(connectionString))
   {
       _dbContext.Ducks.Add(duck);
       _dbContext.SaveChanges();
   }
}

Działa też z await using.

async Task SaveDuck(Duck duck)
{
   if (duck.Color == Color.Pink)
   {
       throw new ArgumentException("We don't serve your kind here!", nameof(duck));
   }

   await using var _dbContext = new DbContext(connectionString);
   _dbContext.Ducks.Add(duck);
   await _dbContext.SaveChangesAsync();
}

6.2 Static local functions

Funkcje lokalne, tylko statyczne.

async Task SaveDuck(Duck duck)
{
   ThrowIfPink(duck);
   await using var _dbContext = new DbContext(connectionString);
   _dbContext.Ducks.Add(duck);
   await _dbContext.SaveChangesAsync();

   static void ThrowIfPink(Duck duck)
   {
	   if (duck.Color == Color.Pink)
	   {
	       throw new ArgumentException("We don't serve your kind here!", nameof(duck));
	   }
   }
}

Statyczna metoda nie łapie żadnych zmiennych ze scope’u i pozwala lepiej wyizolować irrelevant details.

6.3 Null-coalescing assignment

duck ??= new Duck();

is equivalent to:

duck = duck ?? new Duck();

albo inaczej

if (duck == null)
{
    duck = new Duck();
}

6.4 The most important feature ever

Before C#8 napisanie czegoś takiego:

@$"Look at me, interpolated and verbatim at the same time!"

było karane śmiercią. Trzeba było pisać $@. Teraz już można. Nie musicie dziękować.

Koniec

Dziękuję za uwagę.

Copyright © Mateusz Gienieczko 2019