Поскольку Dart 3 находится в стадии альфа, некоторые детали могут ещё меняться, но общие настройки и большинство деталей должны остаться неизменными. Можно уже сейчас попробовать новые возможности.
Дополнительные обновления можно отслеживать на официальном сайте records-feature-specification и feature-specification.md.
Record и Patterns как крупные нововведения Dart 3 являются объектом пристального внимания разработчиков Flutter и Dart.
Кратко говоря, Record позволяет эффективно и компактно создавать анонимные составные значения без необходимости объявления класса, а где объединены данные Record, там Patterns позволяют распаковать эти составные данные до их составных частей.
Известно, что язык Dart всегда был "относительно консервативным", однако поддержка Record и Patterns является полной и глубокой, с возможностью рекурсивного соответствия и условий проверки, что значительно повышает продуктивность для разработчиков Flutter.
Конечно, это также может привести к значительному увеличению количества ошибок.
Как показано ниже, Record представляет собой анонимный неизменяемый тип данных, аналогично Map и List, но Record имеет фиксированную размерность и более гибкое объединение различных типов данных.```dart var record = (1, a: 2, 3, b: 4);
> В отличие от Map и List, Record поддерживает хранение различных типов данных, то есть вам больше не придётся использовать такие конструкции, как `List<Object>` для хранения разнотиповых данных.
Конечно, вы можете спросить, в чём разница между использованием Record и объявлением класса для хранения различных объектов? Разница существенная:
- Объявление класса требует связывания ваших данных с конкретным классом;
- При использовании Record нет необходимости объявлять тип, **Dart будет считать записи одного типа, если они имеют одинаковое количество полей** (это будет подробнее рассмотрено позже).
> Таким образом, можно сказать, что Record представляет собой важное расширение возможностей для Dart, хотя для других языков программирования это может быть не таким новым понятием.## Краткое введение
Для Record'ов мы расширяем предыдущий код, выводя значения через принт, что позволяет наглядно видеть способы получения значений внутри Record'а: **через позиционные поля с префиксом `$` или именованные поля**.
```dart
var record = (1, a: 2, 3, b: 4);
print(record.$1); // Выводит "1"
print(record.a); // Выводит "2"
print(record.$2); // Выводит "3"
print(record.b); // Выводит "4"
В истории изменения Record'ов было объявлено: начальный позиционный индекс Record'а теперь начинается с
$1
, а не с$0
, однако на DartPad вы можете столкнуться с необходимостью использования начального индекса$0
.Определение Record'ов осуществляется с помощью()
и,
. Почему используется,
, показано ниже:
var num = (123); // num
var records = (123,); // record
,
выражение (123)
будет считаться объектом типа num
.,
выражение (123,)
будет распознано как Record.Как коллекция типов, Record также может использоваться для объявления переменных, например:
(bool, num, {int n, String s}) records;
records = (false, 1, n: 12, s: "xxx");
print(records);
Конечно, если присвоить значение таким образом, как показано ниже, вы получите ошибку не может быть присвоено переменной указанного типа
, так как они имеют разные типы. Record имеет фиксированную длину:
records = (false, 1, s: "xxx2");
records = (false, 1, n: 12);
Именованные поля Record позволяют присваивать значения следующим образом:
records = (false, 1, s: "xxx2", n: 12);
records = (s: "xxx2", n: 12, false, 1);
print(records);
Наконец, при определении Record следует придерживаться следующих правил:
s
невозможны.(,)
недопустимо, но ()
можно использовать для константы пустого Record.(6)
не является Record, так как отсутствует запятая после скобок.hashCode
, runtimeType
, noSuchMethod
, toString
быть не должно._
, быть не должно.('pos', $1: 'named')
, быть не должно, но ($1: 'records')
допустимо.Понимание логики Record'ов помогает понять интересные особенности этого механизма.var t = (int, String);
print(t);
print(t.$0.runtimeType);
print(t.$1.runtimeType);
При печати вы заметите, что $0
и $1
в t
имеют тип _Type
. То есть, если после этого присвоить значение t = (1, "fff")
, получится следующая ошибка:
На самом деле этот пример не имеет практического значения, но важно отметить различие между
var t = (int, String)
и(int, String) t
.
Наконец, коротко рассмотрим типовое отношение Record'ов:
Если расширить сравнение Record'ов друг с другом, допустим, имеются два Record'a A и B, где B имеет ту же форму, что и A, и все его поля являются подтипами полей A, тогда можно считать Record B подтипом Record A.
Ранее мы уже говорили, что в Record'ах записи с одинаковым набором полей считаются одного типа. Как это понять?
Сначала следует отметить, что порядок названий полей в Record типах не важен, то есть {int a, int b}
и {int b, int a}
будут иметь одинаковую систему типов и поведение во время выполнения.
Также стоит отметить, что позиционные поля не ограничиваются только именами
$1
,$2
и т.д.,'a'
,'b'
и('$1': 'a', '$2': 'b')
имеют одинаковые члены, просто они отличаются формой.Например, сигнатура(1.2, name: 's', true, count: 3)
может выглядеть следующим образом:
class extends Record {
double get $1;
String get name;
bool get $2;
int get count;
}
Каждое поле в Record имеет только геттер, поэтому данные Record неизменяемы, и нет сеттеров.
Из-за сложности данных Record, его идентификатором служит его содержание, значит две Record'ы с одинаковой формой и полями равны.
print((a: 1, b: 2) == (b: 2, a: 1)); // true
Однако, если порядок позиционных параметров различается, как в этом случае, они не будут равны из-за различной формы, и будет выведено false
.
print((true, 2, a: 1, b: 2,) == (2, true, b: 2, a: 1)); // false
При этом, тип Record во время выполнения определяется типами его полей во время выполнения, как показано ниже:
(num, Object) pair = (1, 2.3);
print(pair is (int, double)); // "true".
Здесь во время выполнения pair
имеет тип (int, double)
, а не (num, Object)
, хотя это указано в официальной документации, но интересно проверить это на Dartpad:
Рассмотрим ещё один пример, представленный следующим кодом, где Record может использоваться в качестве ключа в Map, так как их форма и значения равны, что позволяет извлекать значения из Map.
var map = {};
map[(1, "aa")] = "значение";
print(map[(1, "aa")]); // выводит "значение"
Если мы определим новый класс NewClass
, как показано ниже, можно предположить, что результат будет null
, поскольку два объекта NewClass
не равны.```dart
class NewClass {
}
var map = {}; map[(1, new NewClass())] = "значение"; print(map[(1, new NewClass())]); // выводит "null"
Однако если переопределить методы `==` и `hashCode` в классе `NewClass`, то результат будет `"значение"`.
```dart
class NewClass {
@override
bool operator ==(Object other) {
return true;
}
@override
int get hashCode => 1111111;
}
Итак, теперь вы должны понять значение фразы «Dart считает записи с одинаковым набором полей одного типа».
Наконец, рассмотрим ещё одну особенность времени выполнения, поля Record вычисляются слева направо, даже если последующие реализации выбирают другое порядковое расположение именованных полей, как показано ниже:
int say(int i) {
print(i);
return i;
}
var x = (a: say(1), b: say(2));
var y = (b: say(3), a: say(4));
Вышеуказаный код обязательно выведет "1", "2" / "3", "4". Даже при следующем порядке вызова, результат будет "0", "1", "2" / "3", "4", "5".
var x = (say(0), a: say(1), b: say(2));
var y = (b: say(3), a: say(4), say(5));
Поскольку Records в Dart 3 были улучшены на основе предыдущих версий, некоторые вопросы совместимости грамматики являются необходимыми. Вот список наиболее распространённых корректировок, предоставленных официально.### try/on
Сначала рассмотрим синтаксис try/on
. Если следовать старым правилам, вторая строчка on
должна была распознаваться как локальная функция, но после введения Record типов, теперь это может быть совпадение типа on
Record.
void recordTryOn() {
try {
} on String {
}
on(int, String) {
}
}
```
> Здесь объявленные типы не имеют особого значения, они используются только для наглядной демонстрации сравнения.
Для того чтобы устранить двусмысленность, если использовать версию Dart, которая не поддерживает Record типы, то использование ключевого слова `on` с круглыми скобками будет распознано как попытка использования Record типа, что приведёт к ошибке синтаксиса.

### аннотация metadata
Как показано ниже, наличие Record типов может вызвать дополнительные синтаксические неясности при использовании аннотаций:
```dart
@metadata (a, b) function() {}
```
Если не установить правила понимания, это может быть:
- Аннотация `@metadata(a, b)` связана с объявлением функции без возвращаемого типа
- Аннотация `@metadata` связана с функцией, возвращающей Record тип `(a, b)`
Поэтому здесь используется пробел для различия, хотя это легко может привести к ошибкам:
```dart
@metadata(a, b) function() {}
@metadata (a, b) function() {}
```
- В первом случае отсутствие пробела после `@metadata` указывает на то, что `(a, b)` является аннотацией метаданных
- Во втором случае наличие пробела указывает на то, что `(a, b)` является типом Record
Различие между ними можно видеть на примерах двух типов:
```dart
// Metadata и Record применяются вместе к a
@metadata(x, y) a;
@metadata<T>(x, y) a;
@metadata <T>(x, y) a;
```// Record применяется непосредственно к a, а metadata нет
@metadata (x, y) a;
@metadata
(x, y) a;
@metadata /* comment */ (x, y) a;
@metadata // Comment.
(x,) a;
```
Например, в этом случае `@TestMeta(1, "2")`, где нет пробела, не возникнет синтаксической ошибки:
```dart
@TestMeta(1, "2")
class C {}
class TestMeta {
final String message;
final num code;
const TestMeta(this.code, this.message);
@override
String toString() => "feature: $code, $message";
}
```
Однако если использовать `@TestMeta (1, "2")`, то возникнет ошибка `Annotations can't have spaces or comments before the parenthesis.`
```dart
@TestMeta (1, "2") // Error
class C {}
```
Поэтому наличие или отсутствие пробелов в метаданных аннотаций будет иметь совершенно другое значение, что может привести к неконформному поведению некоторых сторонних плагинов.
### toString
В режиме отладки метод `toString()` для Record выводит значения каждого поля, вызывая метод `toString()` для каждого поля и добавляя имя поля перед значением. Добавление символов `:` зависит от того, является ли поле именованным. В результате каждый атрибут преобразуется в строковое представление.
Пример ниже поможет лучше понять это.
Каждое поле соединяется запятой `,`, и возвращается результат, заключенный в скобках, например:
```
print((1, 2, 3).toString()); // "(1, 2, 3)".
print((a: 'строка', 'число').toString()); // "(a: строка, число)".
```
В режиме отладки порядок именованных полей не определён, но позиционные поля должны следовать в порядке их объявления.> Таким образом, внутренняя реализация `toString()` может свободно выбирать последовательность именованных полей, независимо от порядка создания записи.
В выпусках или оптимизированной сборке поведение `toString()` становится еще менее предсказуемым, поэтому могут быть выполнены действия по сокращению размера кода за счет удаления полных имён именованных полей.
> **Поэтому рекомендуется использовать `toString()` для Record только в целях отладки**, и настоятельно не следует парсить результат вызова `toString()` или полагаться на него для логических проверок, чтобы избежать путаницы.
# Шаблоны
Если рассматривать только Record, то его ценность может быть невелика, но если использовать его вместе с шаблонами, производительность разработки значительно возрастает, **особенно важна поддержка нескольких выходных значений**.

## Краткое описание
**Что касается шаблонов, здесь мы не будем углубляться слишком подробно**, поскольку в настоящее время использование шаблонов в DartPad отключено, а также из-за сложности и возможных проблем семантики, связанных с шаблонами, они всё ещё имеют много неопределённостей.

> По предложению [тут](https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md#summary), кажется, что все возможности не будут выпущены сразу.### Множественные выходные значения
Переходим к теме, известно, что использование Record позволяет нашим методам возвращать несколько значений, как показано в примере кода ниже:
```dart
(double, double) geoCode(String город) {
var широта = // Вычисляем...
var долгота = // Вычисляем...
return (широта, долгота); // Обернуть в запись и вернуть.
}
```
Однако когда нам требуется получить эти значения, мы используем **деструктуризацию значений с помощью шаблонов**, например:
```dart
var (широта, долгота) = geoCode('Аarhus');
print('Местоположение широта:${широта}, долгота:${долгота}');
```
**Конечно, деструктуризация значений с помощью шаблонов не ограничивается только записями**, например для списков или словарей:
```dart
var список = [1, 2, 3];
var [a, b, c] = список;
print(a + b + c); // 6
var словарь = {'первый': 1, 'второй': 2};
var {'первый': a, 'второй': b} = словарь;
print(a + b); // 3
```
Дальнейшее использование позволяет деструктуризировать и присвоить значения существующим переменным:
```dart
var (a, b) = ('слева', 'справа');
(b, a) = (a, b); // Поменять местами!
print('${a} ${b}'); // Выведет "справа слева".
```
> Не кажется ли вам, что код становится менее читаемым? Хахаха
### Алгебраические типы данных
Как было представлено на конференции Flutter Forward, теперь классовые иерархии могут моделироваться с использованием алгебраических типов данных, шаблоны предоставляют новые возможности для паттерн-матчинга, таким образом код может выглядеть следующим образом:```dart
/// До
double рассчитатьПлощадь(Figura фигура) {
if (figura is Квадрат) {
return figura.сторона * figura.сторона; // Исправлено: было "+", должно быть "*"
} else if (figura is Окружность) {
return math.pi * figura.радиус * figura.радиус;
} else {
throw ArgumentError("Неожиданная фигура.");
}
}
// После
double рассчитатьПлощадь(Figura фигура) =>
switch (figura) {
Квадрат(сторона: var l) => l * l,
Окружность(радиус: var r) => math.pi * r * r
};
```
> Даже ключевое слово `switch` больше не требует использования `case`, и используется более простой паттерн, который будет подробнее рассмотрен ниже.
### Шаблоны
На данный момент использование шаблонов в Dart довольно сложное, кратко это можно описать как:
> **Используя компактные, сочетаемые символы, создайте последовательность для проверки объекта на соответствие определенному условию, а затем деконструируйте данные из этого объекта, выполняя код только при выполнении всех этих условий**.
Поэтому вы можете видеть короткие строки кода, состоящие из различных операторов, таких как `"||"`, `"&&"`, `"=="`, `"<"`, `"as"`, `"?"`, `"_"`, `"[]"`, `"( )"` и `{}`, и попытаться понять каждую из них, например:
```dart
var являетсяОсновным = switch (цвет) {
Цвет.красный || Цвет.желтый || Цвет.синий => true,
_ => false
};
```
```dart
switch (shape) {
case Square(size: var s) || Circle(size: var s) when s > 0:
print('Непустая симметричная форма');
case Square() || Circle():
print('Пустая симметричная форма');
default:
print('Асимметрическая форма');
}
```Подобный подход значительно оптимизирует структуру `switch`, как показано ниже:
```dart
String asciiCharType(int char) {
const space = 32;
const zero = 48;
const nine = 57;
return switch (char) {
< space => 'контрольный символ',
== space => 'пробел',
> space && < zero => 'знак препинания',
>= zero && <= nine => 'цифра'
// и т.д.
};
}
```
Конечно, есть и некоторые странные случаи использования, такие как использование `?` для проверки непустых значений, что может быть контринтуитивным. В конечном итоге, решение будет зависеть от обсуждений сообщества:
```dart
String? maybeString = ...;
switch (maybeString) {
case var s?:
// s имеет тип non-nullable String здесь.
}
```
Дальнейшее развитие включает приведение к non-null значениям через `!` при декомпозиции позиций и проверках `switch`. Например, если первое поле списка равно `'user'`, то второе поле должно содержать значение.
```dart
(int?, int?) position = ...;
// Мы знаем, что координаты должны быть присутствовать, если мы дошли до этого места:
var (x!, y!) = position;
List<String?> row = ...;
// Если первое поле равно 'user', то следующее поле должно содержать имя.
switch (row) {
case ['user', var name!]:
// name является non-nullable строкой здесь.
}
```
Если использовать записи вместе с такими шаблонами, это может сделать код ещё более сложным для понимания. Например, в этом примере переменные `var a` и `var b` являются переменными шаблона и будут связаны со значениями `1` и `2`.```dart
switch ((1, 2)) {
case (var a, var b): ...
}
switch (record) {
case (int x, String s):
print('Первое поле - целое число $x, а второе - строка $s.');
}
```
Это аналогично возможностям Flutter Forward, где `case` можно использовать для связывания переменных.

> Если имя переменной `_`, она не связывается ни с чем.
Кроме того, могут использоваться шаблоны для списков, карт, записей, объектов и т.д., **что существенно меняет стиль написания и организацию логики в Dart**.```dart
var list = [1, 2, 3];
var [_, два, _] = list;
var [a, b, ...остаток, c, d] = [1, 2, 3, 4, 5, 6, 7];
print('$a $b $остаток $c $d'); // Выведет "1 2 [3, 4, 5] 6 7".
// Переменная:
var (untyped: untyped, typed: int typed) = ...
var (:untyped, :int typed) = ...
switch (obj) {
case (untyped: var untyped, typed: int typed): ...
case (:var untyped, :int typed): ...
}
// Проверка на null и утверждение:
switch (obj) {
case (checked: var checked?, asserted: var asserted!): ...
case (:var checked?, :var asserted!): ...
}
// Преобразование типов:
var (поле: поле как int) = ...
var (:поле как int) = ...
class Прямоугольник {
final double ширина, высота;
Прямоугольник(this.ширина, this.высота);
}
void отображение(Object obj) {
switch (obj) {
case Прямоугольник(ширина: var w, высота: var h): print('Прямоугольник $w x $h');
default: print(obj);
}
}
```
> На данный момент выглядит так, что **это будет такой функционал, который будет удобен при написании, но сложен для понимания другими**, а также может привести к множеству обратных совместимостей, более подробно см.: [patterns-feature-specification](https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md).Хорошо, относительно Patterns здесь всё, его окончательная реализация пока ещё неопределена, но с моей точки зрения, это однозначно двусторонний меч, надеюсь, что Patterns не приведут к появлению большого количества ошибок.
# Последнее
На самом деле, я уверен, что большинство людей будут интересоваться в основном Record и деконстрюцией значений, чтобы иметь возможность возвращать несколько значений из функций, что является наиболее очевидным и практичным подходом для нас.
Что касается того, как работает switch для соответствия и как Patterns могут упрощать структуру кода, то это уже вторично.
Теперь, или вы можете выбрать Dart 3 и попробовать новые возможности.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )