Dart Record (Запись / Кортеж)

I. Что такое Record?

Record — это анонимный, неизменяемый составной агрегатный тип, предназначенный для объединения данных разных типов в один объект. Основные отличия от List / Set / Map:

  • Фиксированная длина: после создания количество полей нельзя увеличить или уменьшить, гетерогенное хранение:
  • В одном Record можно хранить совершенно разные типы: int, String, bool и другие
  • Строгая типизация: компилятор распознаёт тип каждого поля, безопасность типов не теряется
  • Только чтение, без изменений: после создания значения полей переназначить нельзя

Record является полноценной переменной, поддерживает все стандартные операции: сохранение в переменных, передачу как аргумент / множественные возвращаемые значения функции, вложенность, помещение в List/Map/Set.

II. Базовый синтаксис Record

1. Литерал Record

Оборачивается круглыми скобками, разделяется запятыми, делится на две группы: позиционные поля и именованные поля. Позиционные поля пишутся первыми, именованные — после с синтаксисом ключ:.

// Смесь позиционных и именованных полей
var info = ('Jim', age: 20, flag: true, 'Student');Code language: JavaScript (javascript)

2. Использование Record

1 Позиционные поля

// Объявление: два позиционных поля, последовательно String, int
(String, int) user;

// Присвоение по структуре
user = ('李四', 22);
Code language: JavaScript (javascript)

2 Именованные поля

Объявление типа использует фигурные скобки {}

// Объявить два именованных поля a(int), b(bool)
({int a, bool b}) data;

data = (a: 100, b: false);Code language: JavaScript (javascript)

3 Смешанное использование

Комбинированное объявление типа с позиционными и именованными полями

// 1 позиционное поле String, 2 именованных поля score(int), pass(bool)
(String, {int score, bool pass}) exam;
exam = ('Итоговый экзамен', score: 90, pass: true);
Code language: JavaScript (javascript)

III. Частые ошибки

1: Имя именованного поля является частью структуры типа, разные имена означают полностью разные типы
// Тип: ({int a, int b})
({int a, int b}) pointAB = (a: 1, b: 2);
// Тип: ({int x, int y})
({int x, int y}) pointXY = (x: 3, y: 4);

// Ошибка компиляции! Типы не совпадают, взаимное присваивание запрещено
// pointAB = pointXY;
Code language: JavaScript (javascript)

Для именованных полей даже при двух полях типа int взаимное присваивание невозможно из-за разных имён ab и xy. Для позиционных полей смотрите ниже.

2: Псевдонимы позиционных полей в аннотации типа — только комментарии, не влияют на сопоставление типов

Имена, присвоенные позиционным полям внутри скобок, нужны только для документации разработчика и игнорируются компилятором. Если структуры совпадают, присваивание разрешено:

(int a, int b) p1 = (10, 20);
(int x, int y) p2 = (30, 40);

// Допустимо, под капотом два позиционных поля int, типы полностью совпадают
p1 = p2;Code language: JavaScript (javascript)

Аналогия с параметрами функции: переименование параметров не меняет сигнатуру функции, логика одинаковая.

IV. Чтение внутренних полей Record

Record неизменяемый, есть только геттеры, сеттеров нет — изменить значения полей нельзя.

  1. Именованные поля: прямой доступ через .имяПоля
  2. Позиционные поля: доступ по $цифра; цифра игнорирует все именованные поля и считает только порядок позиционных полей
var record = ('first', a: 2, b: true, 'last');

print(record.$1); // Первое позиционное поле: first
print(record.a);  // Именованное поле a: 2
print(record.b);  // Именованное поле b: true
print(record.$2); // Второе позиционное поле: last
Code language: PHP (php)

V. Правила определения структурного типа Record

Для Record не нужна отдельная декларация класса; тип определяется по его структурной форме:

Форма = общее количество полей + тип каждого поля + имена именованных полей

Если форма полностью совпадает, то даже при разных файлах и библиотеках объекты относятся к одному типу.

Компилятор постоянно распознаёт статический тип каждого поля, информация о типах не теряется:

// Позиция 1: тип num, позиция 2: тип Object
(num, Object) pair = (42, 'Текстовая строка');

var firstVal = pair.$1; // Статический тип num, во время выполнения int
var secondVal = pair.$2;// Статический тип Object, во время выполнения StringCode language: JavaScript (javascript)

VI. Сравнение равенства Record ==

Два условия для признания объектов равными:

  1. Оба Record имеют полностью одинаковую структурную форму
  2. Все значения соответствующих позиционных / именованных полей равны

Порядок записи именованных полей не влияет на сравнение равенства; но разные имена полей сразу означают разные типы, объекты никогда не будут равны.

// Позиционные поля, одинаковая форма и равные значения → true
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // true

// Разные имена именованных полей, разные типы → false
({int x, int y, int z}) p = (x:1, y:2, z:3);
({int r, int g, int b}) c = (r:1, g:2, b:3);
print(p == c); // falseCode language: PHP (php)

В Record автоматически переопределены оператор == и метод hashCode, ручная реализация не требуется.

VII. Множественные возвращаемые значения функций

Функции Dart могут возвращать только одно значение. С помощью Record группируют данные разных типов, а шаблонная деструктуризация быстро разбивает их на отдельные переменные. Идеальная замена List/Map (без потери безопасности типов).

1 Деструктуризация позиционных полей

// Возвращает: Имя(String), Возраст(int)
(String name, int age) getUserInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10, 'color': 'blue'};
  // Прямая деструктуризация, две переменные в одну строку
  var (userName, userAge) = getUserInfo(userJson);
  print(userName); // Dash
  print(userAge);  // 10
}
Code language: JavaScript (javascript)

2 Деструктуризация именованных полей

Синтаксис :имяПоля

({String name, int age}) getUserInfo(Map<String, dynamic> json) {
  return (name: json['name'] as String, age: json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10};
  // Синтаксис именованной деструктуризации
  final (:name, :age) = getUserInfo(userJson);
  print(name);
}
Code language: JavaScript (javascript)

Минусы альтернативных подходов

В других языках C++, C#, Java без Record приходится выполнять следующие действия:

  1. Создавать новый класс: лишний код, нужно прописывать конструктор и поля;
  2. Возврат через List: потеря типовой безопасности, все значения становятся dynamic;
  3. Возврат через Map: риск опечаток в ключах, отсутствие статической проверки типов.

VIII. Лёгкие структурированные данные

Если нужно только хранить данные без методов, используйте напрямую Record без создания класса. Убирает лишний код объявления класса, подходит для массовых списков данных.

Пример

Список конфигурации кнопок страницы (пример Flutter, станет понятным после изучения Flutter)

import 'package:flutter/material.dart';

void main() {
  final buttonList = [
    (
      label: "Загрузить файл",
      icon: const Icon(Icons.upload_file),
      onPressed: () => print("Нажата кнопка загрузки"),
    ),
    (
      label: "Посмотреть детали",
      icon: const Icon(Icons.info),
      onPressed: () => print("Нажата кнопка деталей"),
    )
  ];
}
Code language: JavaScript (javascript)

IX. typedef: псевдонимы типов для Record

Упрощение длинных типовых записей

Запись типов Record обычно громоздкая. С помощью typedef создают псевдоним для повторного использования и единого изменения структуры полей в будущем.

Базовое использование
// Псевдоним для Record кнопки, onPressed допускает значение null
typedef ButtonConfig = ({
  String label,
  Icon icon,
  void Function()? onPressed
});

// Объявление списка напрямую через псевдоним
List<ButtonConfig> buttons = [
  (label: "Отправить", icon: const Icon(Icons.check), onPressed: () {}),
  (label: "Отмена", icon: const Icon(Icons.close), onPressed: null),
];
Code language: PHP (php)
Дальнейшее расширение

Если позже понадобится добавить методы или инкапсулировать логику для данных, бизнес-код обхода списка не меняется — достаточно заменить только тип:

  1. Вариант 1: замена на обычный класс
class ButtonConfig {
  final String label;
  final Icon icon;
  final void Function()? onPressed;
  ButtonConfig({required this.label, required this.icon, this.onPressed});
  // Добавить собственный метод
  bool get hasClickEvent => onPressed != null;
}
Code language: PHP (php)
  1. Вариант 2: оборачивание исходного Record через Extension Type
extension type ButtonConfig._(({String label, Icon icon, void Function()? onPressed}) _) {
  String get label => _.label;
  Icon get icon => _.icon;
  void Function()? get onPressed => _.onPressed;

  ButtonConfig({required String label, required Icon icon, void Function()? onPressed})
      : this._((label: label, icon: icon, onPressed: onPressed));

  bool get hasClickEvent => _.onPressed != null;
}
Code language: PHP (php)

При обеих вариантах модернизации бизнес-код для циклической отрисовки кнопок на странице остаётся без изменений.

X. Итог

  1. Требование версии: Dart 3.0 и выше;
  2. Три основные особенности: анонимность, неизменяемость, агрегация; фиксированная длина, гетерогенное хранение, строгая типизация;
  3. Два вида полей: позиционные поля $1/$2, именованные поля .имя;
  4. Правило определения типа: тип задаётся структурной формой, имена именованных полей входят в форму; псевдонимы позиционных полей — только комментарии;
  5. Сравнение равенства: одинаковая форма + все значения полей совпадают → true;
  6. Главные преимущества: множественные возвращаемые значения функций, лёгкий контейнер для данных, полная типовая безопасность;
  7. Способы расширения: typedef для упрощения псевдонимов типов, бесшовный переход на class / extension type в будущем.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *