Привет с Google I/O 2023. Сегодня в прямом эфире из Mountain View мы анонсируем Dart 3 — крупнейший релиз Dart на сегодняшний день! Dart 3 содержит три мажорных улучшения. Во-первых, мы завершили путь к 100-процентной нулевой безопасности. Во-вторых, мы добавили новые языковые возможности: записи (records), паттерны (patterns) и модификаторы классов (class modifiers). В-третьих, мы даем предварительный прогноз на будущее, в котором расширим поддержку наших платформ, добавив нативный код для web с помощью Wasm-компиляции. Давайте углубимся в детали.


100% sound null safety

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

Со 100% нулевой безопасностью в Dart у нас есть звуковая система типов (sound null safety — можно перевести как надежная «нулевая» безопасность – прим. пер.). Вы можете быть уверены, что если тип говорит, что значение не равно null, то оно никогда не будет null. Это позволяет избежать определенных классов ошибок при программировании, таких как исключения нулевого указателя (null pointer exceptions). Это также позволяет нашим компиляторам и средам выполнения оптимизировать код так, как это невозможно без нулевой безопасности. Этот выбор дизайна предполагал компромисс. Хотя миграция стала немного сложнее, мы считаем, что сделали правильный выбор в пользу Dart.

Переход на Dart 3

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

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

Мажорные возможности языка – Record, patterns, class modifiers

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

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

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

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

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

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

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

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

Records упрощают процесс создания структурированных данных. Это не заменяет использование классов для построения более формальных иерархий типов. Они просто предлагают другой вариант. В любом случае, для работы со структурированными данными вам, возможно, потребуется разбить их на отдельные элементы. Именно здесь в игру вступает pattern matching (сопоставление с образцом – прим. пер.).

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

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

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

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

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

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

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

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

  default:
    invalid();
}

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

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

Мощной особенностью patterns является возможность проверки на "exhaustiveness" ("исчерпываемость" – прим. пер.), Эта особенность гарантирует, что switch обрабатывает все возможные случаи. В предыдущем примере мы обрабатываем все возможные значения weekday, которое является int. Мы исчерпываем все возможные значения через комбинацию операторов соответствия для конкретных значений 1, 6 или 7, а затем default case по умолчанию _ для остальных случаев. Чтобы включить эту проверку для определяемых пользователем иерархий данных, таких как иерархия классов, используйте модификатор 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 тоже могут использовать patterns. В следующем примере мы используем сопоставление if-case с map-pattern для деструктуризации карты 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!');
}

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

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

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

Модификаторы классов позволяют авторам API поддерживать только определенный набор возможностей. Однако значения по умолчанию остаются неизменными. Мы хотим, чтобы Dart оставался простым и доступным. Итак, как и раньше, обычные классы могут быть constructedextended и implemented (созданы, расширены и реализованы), как показано в следующих примерах:

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

Records, patterns и class modifiers – это очень большие новые возможности, поэтому вполне возможно, что их дизайн можно улучшить. Мы продолжим следить за вашими отзывами и посмотрим, нужны ли обновления в минорных выпусках после Dart 3.

Мы также рассматриваем некоторые более мелкие, более инкрементные возможности, которые точно не сломают систему и направлены на повышение производительности разработчиков без затрат на миграцию. Два примера, которые мы изучаем, это inline classes для обертывания существующих типов c помощью zero-cost “wrappers”, а также primary constructors; возможность, которая вводит более лаконичный синтаксис для определения классов с несколькими полями и основным конструктором.

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

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

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

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

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

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

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

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

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

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

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

Заключение

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

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

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


Материал переведён Ruble.

TODO: change after

Забавная группа с неадекватным автором ☜(゚ヮ゚☜)

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