Мы рассмотрим Records, изучим их возможности, а также приведем примеры использования. Будь Вы начинающий или опытный разработчик Dart, эта статья даст вам понимание того, как записи могут помочь вам писать более качественный код.

Многие языки программирования имеют такой тип, как кортеж (tuple или product):

var tuple = (“a”, true);

Кортеж — это упорядоченный список позиционных полей.

Эти языки также скорее всего имеют тип Record. Отличие записей от кортежей состоит в том, что поля в записи не упорядочены и являются именованными.

В Dart же записи могут иметь только позиционные параметры, либо только именованные, либо и то и другое.

Что такое Record в Dart?

Record — это новый тип данных в Dart. Он предоставляет лаконичный синтаксис для объявления классов, которые являются простыми носителями постоянных, неизменяемых наборов данных. С помощью Record вы можете легко создавать структуры данных, которые объединяют существующие данные. Например, чтобы вернуть пару значений:

(double lat, double long) myLocation(String name) {
  // Логика получения локации

  return (56.145748, 47.252178);
}

Enum в Dart является особой формой класса, которая реализует определенный шаблон, но с минимальной синтаксической избыточностью — компилятор генерирует множество кода за нас.

Точно также, Record в Dart является особой формой класса, который реализует шаблон носителя данных с минимальным синтаксисом. Весь избыточный код будет автоматически сгенерирован компилятором.

Record является подтипом Object и dynamic и супертипом для Never. Все записи являются подтипами Record и супертипами Never. Поля в Record неизменяемы, но они могут содержать ссылки на изменяемые объекты. Он также предоставляет реализации hashCode(), == и toString()

Подобно List и Map, Record позволяет объединить несколько значений в один объект. В отличие от других типов коллекций, имеет фиксированный размер и гетерогенен. Каждый элемент в Record может иметь свой тип.

Синтаксис

Синтаксис очень похож на список аргументов функции:

var record = (true, name: ‘Bill’, 2)

К полям записи можно получить доступ с помощью геттеров. Каждое именованное поле предоставляет геттер с таким же именем, а для позиционных полей предоставляются геттеры с именами $0, $1, $2 и т.д. Пример:

const city = (56.145748, 47.252178, name: 'Чебоксары');

print('lat: ${city.$0}'); // lat: 56.145748
print('long: ${city.$1}'); // long: 47.252178
print('city: ${city.name}'); // city: Чебоксары

Если некоторые ограничения при выборе имени для поля записи:

  • Не стоит использовать имя поля более одного раза.

  • Запись только с одним позиционным полем должна иметь завершающую запятую:

var string = ('I am String!');
var record = ('I am Record!',);
  • Имя поля не может начинаться с _

  • Поле не может иметь имя – hashCoderuntimeTypenoSuchMethod или toString

  • Имя не должно конфликтовать с синтезированным именем геттера позиционного поля

В данном примере именованное поле $0 конфликтует с геттером первого позиционного поля.

var record = (0, $0: 1, 2);

Аннотация типов

В Dart каждая запись имеет соответствующий тип, который похож на список параметров функции:

(int, String name, bool) record;

Каждое поле представляет собой аннотацию типа и необязательное имя, которое будет полезно для документации.

Именованные поля помещаются в фигурные скобки, почти также как в функциях, но без required и необязательных параметров. Просто тип поля и имя:

({int age, String name}) employee = (age: 40, name: 'Bill');

Также в аннотации записи можно использовать как позиционные, так и именованные поля:

(bool, num, {int n, String s}) pizza;

Вот как будет выглядеть объявление пустой записи без полей:

() emptyRecord;

Пример без Record

Здесь для объяснения записей мы будем использовать в качестве примера класс, содержащий данные о локации пользователя. Мы увидим, как c помощью Record вы сможете сделать свой код менее подробным и более простым.

Давайте посмотрим, как может выглядеть класс Location без использования записей:

class Location {
  final double lat;
  final double long;

  Location(this.lat, this.long);

  @override
  String toString() {
    return 'Location(lat: $lat, long: $long)';
  }

  @override
  bool operator ==(dynamic other) {
    return identical(this, other) ||
        (other.runtimeType == runtimeType &&
            other is Location &&
            (identical(other.lat, lat) || other.lat == lat) &&
            (identical(other.long, long) || other.long == long));
  }

  @override
  int get hashCode => Object.hash(runtimeType, lat, long);
}

Далее объект Location может быть создан следующим образом:

class LocationService {

  Future<Location> getUserLocation() async {
    // Логика получения координат

    return Location(56.145748, 47.252178);
  }
}

Пример с Record

Давайте посмотрим, как Record изменит ситуацию:

var location = (lat: 56.145748, long: 47.252178);

Создав эту запись, мы заметно уменьшили количество кода и повысили удобочитаемость.

Верхняя и нижняя границы

Вычисления границ для типов записей включены в основную спецификацию здесь. Кратко:

Если 2 записи имеют одинаковую форму, то их наименьшей верхней границей является новый тип записи той же формы, где тип каждого поля является наименьшей верхней границей соответствующего поля в исходных типах:

(num, String) a = (1.2, "s");
(int, Object) b = (2, true);
var c = condition ? a : b; // c имеет тип `(num, Object)`.

Нижняя граница двух типов записей с одинаковой формой — это наибольшая нижняя граница составляющих их полей:

a((num, String) record) {}
b((int, Object) record) {}
var c = condition ? a : b; // c имеет тип `Function((int, String))`.

Наименьшей верхней границей двух типов записей с разными формами является Record:

(num, String) a = (1.2, "s");
(num, String, bool) b = (2, "s", true);
var c = condition ? a : b; // c имеет тип `Record`.

Наибольшая нижняя граница записей различной формы равна Never.

Резюме

Запись в Dart - это новый тип данных, обеспечивающий лаконичный синтаксис для объявления классов, которые действуют как простые носители постоянных, неизменяемых наборов данных. Их можно использовать в таких случаях, как множественные возвраты, объединение нескольких объектов, составные ключи, DTO и т.д. В целом, записи в Dart - это мощный и гибкий инструмент, который может упростить работу с данными в ваших приложениях.

Материал подготовил Денис Петров | AppFox.ru специально для Habr.com

Комментарии (8)


  1. Sayonji
    00.00.0000 00:00

    Деструктиризация есть?


    1. aaabramenko
      00.00.0000 00:00
      +2

      Да Introducing Dart 3 alpha

      Where records allow you to combine data, patterns can destructure composite data into its constituents. For example, to destructure the return value of geoLocation above (a record consisting of a pair of ints) into two individual int variables, lat and long, you can use a pattern declaration like this:

      void main(List<String> arguments) {
        final (lat, long) = geoLocation('Nairobi');
        print('Current location: $lat, $long');
      }


  1. Zalexei
    00.00.0000 00:00

    Делает ли это код более грязным? Не логичнее было бы сделать упрощённое созданиие классов типа class Location(int lat, int lon) наподобие Котлину?


    1. Sayonji
      00.00.0000 00:00

      Отличие в приведении типов. В переменную типа (num, String) можно присвоить тип (int, String), с классами так не сработает. Нужно было бы кучу классов добавлять: Tuple2<A, B>, Tuple3<A, B, C>, ..., вот эти записи по сути оно и есть, только с синтаксическим сахаром.


  1. Cobalt
    00.00.0000 00:00
    +1

    Самое важное ни где не упомянуто - это работает только в альфа 3.0


  1. aaabramenko
    00.00.0000 00:00
    +2

    Хочется уже data-классов и забыть про freezed.


  1. paveltyurikov
    00.00.0000 00:00

    В Dart же записи могут иметь только позиционные параметры, либо только именованные, либо и то и другое

    Это как вообще?


    1. PackRuble
      00.00.0000 00:00

      Ну таки вот:

      (int, bool) onlyPos;
      (String name) onlyRec;
      (int, String name, bool) posAndRec;