v1.0.0 Copyright © Mateusz Gienieczko 2019
IAsyncEnumerable
/await foreach
oraz IAsyncDisposable
/await using
switch
i pattern matchingIndex
i Range
The billion dollar mistake fixed! Well, kinda.
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.
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!);
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.
Yyy, zawsze.
Czego nie zrozumieliście. Dosłownie nie ma powodu żeby tego nie włączyć w nowych projektach.
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.
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!”.
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();
}
}
Of course it can. Wystarczy pobrać paczkę System.Linq.Async
.
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!!!
Time for the elephant in the room.
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.
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).
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.
switch
i pattern matchingLet’s talk about something more joyful: functional programming!
Patterny to coś czego można użyć razem z pattern expression. Przed C#8 jedynym pattern expression był is
. Składnia is
a 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.
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.
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));
}
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))
};
var
patternTen 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.
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ł.
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
};
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.
Index
i Range
You think we can only steal from be inspired by functional languages? Python slicing here we go.
Index
Index
to indeks.
Range
Range
to para indeksów.
Wyrażenie ^i
oznacza i
-ty od końca.
x..y
to Range
od indeksu x
do y
.
Yes.
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
Tak szybko M$ je implementuje. Oto paczka małych QoL changes.
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();
}
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.
duck ??= new Duck();
is equivalent to:
duck = duck ?? new Duck();
albo inaczej
if (duck == null)
{
duck = new Duck();
}
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ć.
Dziękuję za uwagę.
Copyright © Mateusz Gienieczko 2019