Эта статья — перевод оригинальной статьи "Announcing Dart 3".

Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

Привет из Google I/O 2023. Сегодня, в прямом эфире из Маунтин-Вью, мы объявляем о выпуске Dart 3 - самого крупного релиза Dart на сегодняшний день! Dart 3 содержит три основных усовершенствования. Во-первых, мы завершили путь к 100% надёжная null безопасности. Во-вторых, мы добавили новые возможности языка для записей, шаблонов и модификаторов классов. В-третьих, мы заглядываем в будущее, где мы расширим поддержку наших платформ, добавив нативный код для веба с помощью Wasm-компиляции. Давайте разберемся во всём в деталях.

100% надёжная null безопасность

За последние четыре года мы превратили Dart в быстрый, переносимый и современный язык. Теперь, с выходом Dart 3, это на 100% null безопасный язык! Как мы уже говорили ранее, мы не верим, что какой-либо другой язык программирования когда-либо добавлял надежную защиту null в существующий язык. Так что это было очень увлекательное путешествие.

Благодаря 100% безопасности null в Dart мы имеем надежную систему типов. Вы можете быть уверены, что если тип говорит, что значение не является null, то оно никогда не может быть null. Это позволяет избежать определенных классов ошибок, таких как исключения нулевого указателя. Это также позволяет нашим компиляторам и программам выполнения оптимизировать код так, как это было бы невозможно без null безопасности. Этот выбор дизайна подразумевает компромисс. Хотя миграции стали немного сложнее, мы считаем, что сделали правильный выбор для Dart.

Переход на Dart 3

Критической частью в достижении надежной null безопасности стала непоколебимая поддержка со стороны сообщества Dart: 99% из 1000 лучших пакетов на pub.dev поддерживают null безопасность!

Учитывая это, мы ожидаем, что подавляющее большинство пакетов и приложений, которые были переведены на null безопасность, будут работать с Dart 3. Лишь в некоторых случаях небольшое количество сопутствующей очистки в Dart 3 может повлиять на некоторый код. Некоторые устаревшие API основных библиотек были удалены (#34233, #49529), а некоторые инструменты были скорректированы (#50707). Если у вас возникнут проблемы с переходом на использование Dart 3 SDK, пожалуйста, обратитесь к руководству по переходу на Dart 3. В остальном мы надеемся, что вам понравятся новые рационализированные основные библиотеки и инструменты.

Новые возможности языка - записи, шаблоны и модификаторы класса

Dart 3 - это не просто изменение существующего языка. Это также добавление новых значимых функций и возможностей! К ним относятся записи, шаблоны и модификаторы классов.

Построение структурированных данных с помощью записей

Традиционно функция Dart могла возвращать только одно значение. В результате функции, которым требовалось вернуть несколько значений, должны были либо упаковывать их в другие типы данных, такие как Map или List, либо определять новые классы, которые могли бы хранить эти значения. Использование нетипизированных структур данных ослабляло безопасность типов. Необходимость определять новые классы только для переноса данных добавляет трудности в процессе написания кода. Вы дали нам это понять: запрос языка на множественные возвращаемые значения - четвертая по рейтингу тем.

С помощью записей можно создавать структурированные данные с красивым и четким синтаксисом. Рассмотрим эту функцию. Она считывает имя и возраст из JSON-блоба и возвращает их в виде записи:

(String, int) userInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['height'] as int);
}

Это должно выглядеть знакомо всем разработчикам Dart. Запись выглядит как литерал списка, например ['Michael', 'Product Manager'], но вместо скобок используются круглые скобки. В Dart записи - это общая характеристика. Их можно использовать не только для возврата значений функций. Вы также можете хранить их в переменных, помещать в список, использовать в качестве ключей в карте или создавать записи, содержащие другие записи. Вы можете добавлять как безымянные поля, как мы делали в предыдущем примере, так и именованные поля, например (42, description: 'Meaning of life').

Записи являются типами значений и не имеют идентификатора. Это позволяет нашим компиляторам в некоторых случаях полностью удалять объект записи. Записи также поставляются с автоматически определяемым оператором == и функциями hashCode. Более подробная информация содержится в документации по записям.

Работа со структурированными данными с использованием шаблонов и сопоставления шаблонов

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

Рассмотрим базовую форму шаблона. Следующий шаблон записи деструктурирует запись на две новые переменные name и height. Затем эти переменные можно использовать как любые другие переменные, например, в вызове функции print:

var (String name, int height) = userInfo({'name': 'Michael', 'height': 180});
print('User $name is $height cm tall.');

Аналогичные шаблоны существуют для List и Map. Во всех этих случаях можно пропустить отдельные элементы с помощью шаблона с подчеркиванием:

var (String name, _) = userInfo(…);

Паттерны особенно хорошо работают с оператором switch. С самого начала Dart имел ограниченную поддержку switch. В Dart 3 мы расширили возможности и выразительность оператора switch. Теперь мы поддерживаем сопоставление шаблонов в кейсах. Мы избавились от необходимости добавлять break в конце каждого кейса. Мы также поддерживаем логические операторы для объединения кейсов. В следующем примере показан красивый и четкий оператор switch, который анализирует символьный код:

switch (charCode) {
  case slash when nextCharCode == slash:
    skipComment();

  case slash || star || plus || minus:
    operator(charCode);

  case >= digit0 && <= digit9:
    number();

  default:
    invalid();
}

Оператор switch оказывает большую помощь, когда вам требуется один или несколько операторов для каждого кейса. В некоторых кейсах всё, что вам нужно сделать, - это вычислить значение. Для этого случая мы предлагаем очень лаконичное выражение switch. Оно похоже на оператор switch, но использует другой синтаксис, специально разработанный для выражений. Следующий пример функции возвращает значение выражения switch для вычисления описания сегодняшнего дня недели:

String describeDate(DateTime dt) =>
  switch (dt.weekday) {
      1 => 'Feeling the Monday blues?',
      6 || 7 => 'Enjoy the weekend!',
      _ => 'Hang in there.'
  };

Мощной особенностью шаблонов является возможность проверки на "исчерпанность". Эта особенность гарантирует, что переключатель обрабатывает все возможные случаи. В предыдущем примере мы обрабатываем все возможные значения weekday, которое является int. Мы исчерпываем все возможные значения с помощью комбинации операторов соответствия для конкретных значений 1, 6 или 7, а затем используем кейс по умолчанию _ для остальных случаев. Чтобы включить эту проверку для пользовательских иерархий данных, таких как иерархия классов, используйте модификатор new sealed на вершине иерархии классов, как в следующем примере:

sealed class Animal { … }
class Cow extends Animal { … }
class Sheep extends Animal { … }
class Pig extends Animal { … }

String whatDoesItSay(Animal a) =>
    switch (a) { Cow c => '$c says moo', Sheep s => '$s says baa' };

Это возвращает следующую ошибку, предупреждая нас о том, что мы пропустили обработку последнего возможного подтипа, Pig:

line 6 • The type 'Animal' is not exhaustively matched by the switch cases
since it doesn't match 'Pig()'.

Наконец, операторы if тоже могут использовать шаблоны. В следующем примере мы используем сопоставление if-case с шаблоном map для деструктуризации JSON. Внутри него мы сопоставляем постоянные значения (строки типа 'name' и 'Michael') и шаблон проверки типа int h, чтобы считать значение JSON. Если совпадение шаблонов не удалось, Dart выполняет оператор else.

final json = {'name': 'Michael', 'height': 180};

// Find Michael's height.
if (json case {'name': 'Michael', 'height': int h}) {
  print('Michael is $h cm tall.'); 
} else { 
  print('Error: json contains no height info for Michael!');
}

Это только часть того, что можно сделать с помощью паттернов. Мы верим, что они станут широко распространены во всем коде Dart. Чтобы узнать больше, ознакомьтесь с документацией по паттернам и codelab паттернов.

Тонкие элементы управления доступом для классов с модификаторами классов

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

Модификаторы классов позволяют авторам API поддерживать только определенный набор возможностей. При этом значения по умолчанию остаются неизменными. Мы хотим, чтобы Dart оставался простым и доступным. Поэтому, как и раньше, можно создавать, расширять и реализовывать обычные классы, как показано в следующих примерах:

class Vehicle {
  String make; String model;
  void moveForward(int meters) { … }
}

// Construct.
var myCar = Vehicle(make: 'Ford', model: 'T',);

// Extend.
class Car extends Vehicle {
  int passengers;
}

// Implement.
class MockVehicle implements Vehicle {
  @override void moveForward …
}

Модификаторы классов поддерживают добавление ограничений. Рассмотрим несколько примеров использования:

  • С помощью interface class вы можете определить контракт для реализации другими пользователями. Интерфейсный класс не может быть расширен.

  • Используя base class, вы можете гарантировать, что все подтипы вашего класса наследуют от него, а не реализуют его интерфейс. Это гарантирует, что приватные методы будут доступны для всех экземпляров.

  • С помощью final class вы можете закрыть иерархию типов, предотвращая появление подклассов за пределами вашей собственной библиотеки. В качестве примера, это позволяет владельцу API добавлять новые элементы без риска сломать изменения для пользователей API.

Подробности см. в документации по новым модификаторам классов.

Взгляд в будущее

Dart 3 - это не только значимый шаг вперед в плане новых возможностей, которые вы можете использовать уже сегодня. Мы также даем вам предварительную информацию о том, что будет дальше.

Dart language

Записи, шаблоны и модификаторы классов - это очень большие новые возможности, поэтому вполне возможно, что их дизайн можно улучшить. Мы продолжим следить за вашими отзывами и посмотрим, нужны ли обновления в небольших релизах, следующих за Dart 3.

Мы также рассматриваем некоторые более мелкие, более постепенные функции, которые совершенно не ломаются и направлены на повышение производительности разработчиков без затрат на миграцию. Два примера, которые мы изучаем, это встроенные классы, позволяющие обернуть существующие типы с помощью недорогих "оберток", и первичные конструкторы; функция, представляющая более лаконичный синтаксис для определения классов с несколькими полями и первичным конструктором.

Ранее мы уже обсуждали макросы (также называемые мета-программированием). В частности, мы сосредоточились на этом для обеспечения лучшей десериализации JSON (и подобных ему), а также для создания классов данных. Учитывая масштаб и риск, присущий метапрограммированию, мы используем очень тщательный подход, и поэтому у нас нет конкретных сроков, даже для завершения разработки решений.

Нативное взаимодействие

Приложения на мобильных и настольных компьютерах обычно полагаются на множество API, предоставляемых родной платформой, будь то уведомления, платежи или получение местоположения телефона. Традиционно во Flutter доступ к ним осуществляется путем создания плагинов, что требует написания кода Dart для API и кучи специфичного для платформы кода для реализации.

Мы уже поддерживаем взаимодействие с кодом, который компилируется в библиотеки на C с помощью dart:ffi. В настоящее время мы работаем над расширением этой поддержки для взаимодействия с Java и Kotlin на Android, а также Objective C и Swift на iOS/macOS. Для ознакомления с взаимодействием с Android посмотрите новое видео Google I/O 23 по взаимодействию с Android.

Компиляция в WebAssembly - нацеливание на веб с помощью нативного кода

WebAssembly (сокращенно Wasm) становится все более зрелым форматом двоичных инструкций, нейтральным для всех современных браузеров. Фреймворк Flutter использует Wasm уже некоторое время. Именно так мы доставляем графический движок рендеринга SKIA, написанный на C++, в браузер через скомпилированный модуль Wasm. Мы давно хотели использовать Wasm и для развертывания кода Dart, но нам не удавалось это сделать. Dart, как и многие другие объектно-ориентированные языки, использует сборку мусора. За последний год мы сотрудничали с несколькими командами в экосистеме Wasm, чтобы добавить новую функцию WasmGC в стандарт WebAssembly. Сейчас эта функция почти стабильна в браузерах Chromium и Firefox.

Наша работа по компиляции Dart в модули Wasm преследует две цели высокого уровня для веб-приложений:

  • Время загрузки: мы надеемся, что с помощью Wasm мы сможем доставлять полезные нагрузки развертывания, которые браузер сможет загружать быстрее, тем самым улучшая время, необходимое для перехода к моменту, когда пользователь может взаимодействовать с веб-приложением.

  • Производительность: Для достижения хорошей производительности веб-приложениям на JavaScript требуется компиляция "точно в срок". Модули Wasm более низкоуровневые и ближе к машинному коду, поэтому мы считаем, что они могут обеспечить более высокую производительность с меньшим количеством помех и более стабильной частотой кадров.

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

Мы рады объявить о первом предварительном просмотре компиляции Dart в Wasm сегодня! Первоначально мы сосредоточились на поддержке веб-приложений Flutter. Пока еще рано, и нам предстоит еще много работы, но мы приглашаем вас поэкспериментировать и посмотреть, будет ли вам так же интересно, как и нам.

Заключение

Спасибо, что дочитали до конца. Мы надеемся, что этот пост заставил вас с нетерпением ждать выхода Dart 3, который доступен сегодня как в отдельном Dart SDK, так и в Flutter 3.10 SDK.

Мы завершили капитальный пересмотр языка Dart, обеспечив надежную null защиту, а также очистку основных библиотек и инструментов. Появились новые возможности языка, которые делают Dart более выразительным и четким с записями и шаблонами. Для больших поверхностей API модификаторы классов обеспечивают детальный контроль. Мы также включаем предварительный просмотр будущего с нашей грядущей поддержкой WebAssembly.

Благодаря всем этим особенностям, мы считаем, что Dart 3 иллюстрирует наше долгосрочное видение: Создать самый продуктивный язык программирования для создания быстрых приложений на любой платформе. Мы надеемся, что вы тоже так считаете!

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


  1. Cdracm
    30.06.2023 09:11
    +2

    спасибо за перевод, интересно. однако:

    Паттерны сияют, когда используются 

    и не говорите, аж глазам больно. от качества перевода ж)


    1. qmzik Автор
      30.06.2023 09:11

      Спасибо за отзыв! Подскажи, пожалуйста, как лучше перевести

      “Patterns shine when used”


      1. Cdracm
        30.06.2023 09:11

        я думаю, "паттерны особенно хорошо работают с оператором switch"


        1. qmzik Автор
          30.06.2023 09:11

          Заменил