Dart features
Mechanizmy Darta, które warto znać
Dart jest językiem wieloparadygmatowym — pozwala pisać zarówno w stylu imperatywnym, jak i funkcyjnym. Ma wiele przydatnych mechanizmów, o których łatwo zapomnieć, a które mogą ułatwić pisanie prostszego i bardziej niezawodnego kodu. Oto krótkie podsumowanie niektórych ficzerów, o których warto pamiętać.
New Switch-Case
Dla Darta od 3.0
w górę dostępna jest nowa wersja switch-case’a, która działa jako wyrażenie i wspiera pattern matching
oraz guard clauses
. W dokumentacji Dart’a można znaleźć starą składnię switch’a pod nazwą switch statement
a nową jako switch expression
.
Porównanie: switch statement
Widzimy tu ciąg imperatywnych instrukcji, który zostanie wykonany, gdy warunek opisany w case
zostanie spełniony, np.:
var command = 'OPEN';switch (command) { case 'CLOSED': executeClosed(); case 'PENDING': executePending(); case 'APPROVED': executeApproved(); case 'DENIED': executeDenied(); case 'OPEN': executeOpen(); default: executeUnknown();}
Ale co jeśli w wyniku switcha chcemy po prostu przypisać konkretną wartość do danej zmiennej? Korzystając ze switch statement
, będzie wyglądać to tak:
// Where slash, star, comma, semicolon, etc., are constant variables...switch (charCode) { case slash || star || plus || minus: // Logical-or pattern token = operator(charCode); case comma || semicolon: // Logical-or pattern token = punctuation(charCode); case >= digit0 && <= digit9: // Relational and logical-and patterns token = number(); default: throw FormatException('Invalid');}
De facto, jedyne co robimy za każdym razem, to przypisujemy zmiennej wartość, możnaby więc wykonać to szybciej - z pomocą przychodzi switch expression
.
Porównanie: switch expression
Tutaj zaznaczamy, że do zmiennej token
zostanie przypisane cokolwiek zwróci nam wyrażenie switch, które sprowadza się do:
[dopasowanie] => [zwracana wartość].
final token = switch (charCode) { slash || star || plus || minus => operator(charCode), comma || semicolon => punctuation(charCode), >= digit0 && <= digit9 => number(), _ => throw FormatException('Invalid'),};
Krótsze, a przede wszystkim nie wymusza konieczności ręcznego przypisywania wartości do zmiennej pod każdym case
.
Pattern Matching
Pattern matching
w Darcie to potężny mechanizm, który pozwala na dopasowywanie wartości według ich struktury, typu czy zawartości. Wprowadzony w Darcie 3.0
, otwiera nowe możliwości pisania czytelnego i bezpiecznego kodu. Oprócz dopasowywania wartości wewnątrz switch statement
i switch expression
, pozwala też na destructuring
- wyciągnięcie konkretnych pól/atrybutów ze złożonych struktur.
Zalety wykorzystywania pattern matchingu
- Czytelność - kod staje się bardziej deklaratywny i łatwiejszy do zrozumienia.
- Bezpieczeństwo typów - kompilator sprawdza kompletność wzorców.
- Mniej boilerplate - eliminuje potrzebę wielu if-else lub instanceof checks.
- Destructuring - łatwe wyciąganie wartości ze złożonych struktur danych.
- Kompozycja wzorców - możliwość łączenia różnych typów wzorców.
Wykorzystanie: Pattern matching w switch statement
Switch statement
wykonuje różne instrukcje w zależności od dopasowanego typu danych:
switch (value) { case int i when i > 0: print('Positive number'); break; case String s: print('This is a string: $s'); break; default: print('Other type');}
Wykorzystanie: Pattern matching w switch expression
Switch expression
zwraca różne wartości w zależności od dopasowanego typu danych:
String message = switch (value) { int i when i > 0 => 'Positive number', String s => 'This is a string: $s', _ => 'Other type',};
Wykorzystanie: Pattern matching w destructuringu
Pattern matching
można wykorzystać aby wyciągnąć konkretne dane ze struktur, np. z list, tupli, map:
final numList = [1, 2, 3];// List pattern [a, b, c] destructures the three elements from numList...final [a, b, c] = numList;// ...and assigns them to new variables.print(a + b + c);
Wykorzystanie: Użycie wieloznacznika - wildcard
Wildcard
lub też wieloznacznik oznaczany jest jako _
i może pełnić podobną funkcję w switch expression
co default
w switch statement
, np.:
String stringOrInt(Object value) => switch (value) { String s => 'This is a string.', int i => 'This is an int.', _ => 'Neither string nor int.',};
Dodatkowo, przy destructuringu może oznaczać te elementy, które nas nie interesują, np.:
final importantList = ['boring', 'important', 'something', 'something more' ];final [_, i, _, _] = importantList;print('This one is important: $i');
Wildcard
może być również stosowany wtedy, gdy interesuje nas typ zmiennej, ale nie chcemy jej później wykorzystywać, np.:
switch (record) { case (int _, String _): print('First field is int and second is String.');}
Rodzaje wzorców
Constant patterns
Dopasowują dokładną wartość:
String describe(int value) => switch (value) { 0 => 'zero', 1 => 'one', 42 => 'the answer', _ => 'some number',};
Variable patterns
Wiążą dopasowaną wartość z nową zmienną:
String processValue(Object value) => switch (value) { int x when x > 0 => 'positive: $x', int x when x < 0 => 'negative: $x', int x => 'zero: $x', String s => 'text: $s', _ => 'unknown type',};
Record patterns
Dopasowują rekordy według struktury:
String analyzePoint((int, int) point) => switch (point) { (0, 0) => 'origin', (final x, 0) => 'on x-axis at $x', (0, final y) => 'on y-axis at $y', (var x, var y) when x == y => 'diagonal at ($x, $y)', (var x, var y) => 'point at ($x, $y)',};
List patterns
Dopasowują listy według zawartości:
String analyzeList(List<int> numbers) => switch (numbers) { [] => 'empty list', [final single] => 'single element: $single', [final first, final second] => 'two elements: $first, $second', [final first, ...final rest] => 'starts with $first, has ${rest.length} more',};
Map patterns
Dopasowują mapy według kluczy i wartości:
String analyzeUser(Map<String, dynamic> user) => switch (user) { {'name': String name, 'age': int age} when age >= 18 => 'Adult: $name ($age)', {'name': String name, 'age': int age} => 'Minor: $name ($age)', {'name': String name} => 'Name only: $name', _ => 'Invalid user data',};
Object patterns
Dopasowują obiekty według ich właściwości:
sealed class Shape {}class Circle extends Shape { final double radius; Circle(this.radius);}class Rectangle extends Shape { final double width, height; Rectangle(this.width, this.height);}
double calculateArea(Shape shape) => switch (shape) { Circle(radius: final r) => 3.14159 * r * r, Rectangle(width: final w, height: final h) => w * h,};
Jeśli nie ma potrzeby zmieniać nazw pól obiektu w ramach dopasowania, można użyć także takiej notacji:
double calculateArea(Shape shape) => switch (shape) { Circle(:final radius) => 3.14159 * radius * radius, Rectangle(:final width, :final height) => width * height,};