В Dart 3.5 большое новшество: макросы. Это как старая генерация кода, но прямо в памяти, без временных файлов, плюс ещё много преимуществ.
Пока это бета, и документации мало. Вот план команды Dart:
-
Сейчас они выпустили один макрос
@JsonCodable
, который заменяет пакетjson_serializable
и устраняет.g.dart‑файлы. На его примере можно знакомиться с технологией. -
Этот макрос станет стабильным в течение 2024 года.
-
В начале 2025 можно будет писать собственные макросы.
Но оказывается, можно уже сейчас: я написал и опубликовал собственный макрос, и он работает — не надо ждать 2025 года. Можно делать что угодно, только не надо использовать это на проде.
Итак:
-
Разберём пример использования
@JsonCodable
от команды Dart. -
Напишем свой простейший макрос.
-
Глубоко разберём мой макрос, который генерирует парсер параметров командной строки по описанию вашего data‑класса.
Готовим эксперимент
Dart 3.5
Скачайте бета‑версию Dart 3.5 и включите использование макросов по официальной инструкции: https://dart.dev/language/macros#set‑up‑the‑experiment
Я просто скачал ZIP‑файл и положил в отдельную папку.
VSCode
Чтобы видеть код, который макросы создают, нужна последняя стабильная версия плагина Dart для VSCode.
pubspec.yaml
Чтобы использовать @JsonCodable
, нужна версия Dart 3.5.0-154
или выше. Задайте это требование в pubspec.yaml:
name: macro_client environment: sdk: ^3.5.0-154 dependencies: json: ^0.20.2
analysis_options.yaml
Чтобы анализатор не ругался, скажите ему, что экспериментируете с макросами. Для этого создайте такой analysis_options.yaml:
analyzer: enable-experiment: - macros
Код
Скопируйте официальный пример:
import 'package:json/json.dart'; @JsonCodable() // Аннтоация-макрос. class User { final int? age; final String name; final String username; } void main() { // Берём такой JSON: final userJson = { 'age': 5, 'name': 'Roger', 'username': 'roger1337', }; // Создаём объект с полями и выводим: final user = User.fromJson(userJson); print(user); print(user.toJson()); }
Запустите программу с экспериментальным флагом в терминале:
dart run --enable-experiment=macros lib/example.dart
Или настройте VSCode для такого запуска. Откройте settings.json:
И укажите там:
Пример выполнится и напечатает:
Instance of 'User' {age: 5, name: Roger, username: roger1337}
Получается всего 6 строк в классе:
@JsonCodable() class User { final int? age; final String name; final String username; }
А с пакетом json_serializable
это занимало 16 строк:
@JsonSerializable() class User { const Commit({ required this.age, required this.name, required this.username, }); final int? age; final String name; final String username; factory User.fromJson(Map<String, dynamic> map) => _$UserFromJson(map); Map<String, dynamic> toJson() => _$UserToJson(this); }
Как посмотреть сгенерированный код
В VSCode нажмите ссылку «Go to Augmentation» под строчкой, где используется макрос @JsonCodable
. Откроется код:
В отличие от старого генератора это не файл на диске — это всё в памяти, поэтому редактировать нельзя.
Если что‑то поменять в исходном файле, сгенерированный код сразу отразит изменения — ничего не нужно запускать вручную.
А если VSCode вам не подходит, я написал программу, которая показывает точно такой же сгенерированный код.
Как это работает: огментация
В сгенерированном коде используется новая возможность языка: огментация («augmentation», переводится как «дополнение»). Это возможность поменять класс или функцию, добавляя члены и заменяя тела функций за пределами того блока, где они изначально описаны.
Это отдельная синтаксическая конструкция, не связанная с макросами. Вот простейший пример её использования:
class Cat { final String name; // Ошибка "Uninitialized", потому что нет конструктора. } augment class Cat { Cat(this.name); // Исправляем эту ошибку. }
Огментация может быть и в отдельном файле. По сути основная работа макроса — выдать такой файл с огментацией. И главное отличие макросов от старой генерации кода — это то, что всё происходит в памяти без временных.g.dart‑файлов на диске.
Поэтому в принципе можно было бы переделать пакет json_serializable
с использованием огментации и даже без макросов получить такой же короткий код, потому что конструктор можно вынести в огментацию, а форвардеры методов toJson
и fromJson
больше не нужны.
Главные почести — огментации, а не макросам. Да, макросы важны, но их роль вторична в той революции, которая скоро начнётся вокруг языка.
Пишем собственный hello-world макрос
Создайте hello.dart
с кодом макроса:
import 'dart:async'; import 'package:macros/macros.dart'; final _dartCore = Uri.parse('dart:core'); macro class Hello implements ClassDeclarationsMacro { const Hello(); @override Future<void> buildDeclarationsForClass( ClassDeclaration clazz, MemberDeclarationBuilder builder, ) async { final fields = await builder.fieldsOf(clazz); final fieldsString = fields.map((f) => f.identifier.name).join(', '); final print = await builder.resolveIdentifier(_dartCore, 'print'); builder.declareInType( DeclarationCode.fromParts([ 'void hello() {', print, '("Hello! I am ${clazz.identifier.name}. I have $fieldsString.");}', ]), ); } }
Этот макрос создаёт метод hello
в любом классе, к которому мы его применим. Метод печатает название класса и список полей.
Макрос — это класс с модификатором macro
. Здесь мы реализуем интерфейс ClassDeclarationsMacro
, который говорит компилятору, что этот макрос применим к классам и выполняется на том этапе, когда мы генерируем декларации в них. Есть много интерфейсов, которые делают макросы применимыми к разным другим конструкциям и позволяют им работать на других этапах генерации кода. Об этом поговорим, когда будем разбирать макрос для парсинга параметров командной строки.
В интерфейсе есть метод buildDeclarationsForClass
, который нужно реализовать, и он будет вызываться автоматически. Его параметры:
-
ClassDeclaration
с информацией о классе, к которому применяем макрос. -
Билдер, который может анализировать class declaration и добавлять код в класс или глобально в библиотеку.
Мы используем билдер, чтобы получить список полей в классе.
Собственно генерация кода — это просто. У билдера есть метод declareInType
, который дополняет класс любым кодом. В простейшем случае можно передать просто строку, но есть хитрость с функцией print
.
В примере с JsonCodable
выше мы видели, что библиотека dart:core
импортируется с префиксом:
import 'dart:core' as prefix0;
Префикс добавляется автоматически, чтобы ваш код не конфликтовал с символами этой библиотеки. Префикс динамический, и заранее его узнать нельзя. Поэтому вызов print(something)
нельзя написать в коде просто строкой. Поэтому генерируем код из частей:
final print = await builder.resolveIdentifier(_dartCore, 'print'); builder.declareInType( DeclarationCode.fromParts([ // Части: 'void hello() {', print, '("Hello! I am ${clazz.identifier.name}. I have $fieldsString.");}', ]), );
Эти части могут быть строками или ссылками на ранее полученные идентификаторы. В конце всё это будет склеено в строку, и идентификаторы получат нужные префиксы.
Код, который использует наш новый макрос:
import 'hello.dart'; @Hello() class User { const User({ required this.age, required this.name, required this.username, }); final int? age; final String name; final String username; } void main() { final user = User(age: 5, name: 'Roger', username: 'roger1337'); user.hello(); }
Нажмите «Go to Augmentation» и посмотрите, какой код получился:
Обратите внимание, что перед print
появился префикс prefix0
, под которым библиотека dart:core
была импортирована. Кстати, сам импорт был добавлен как побочный эффект включения идентификатора print
в код — мы не делали этот импорт вручную.
Запустите:
dart run --enable-experiment=macros hello_client.dart
Программа напечатает:
Hello! I am User. I have age, name, username.
Настоящие полезные макросы
Можете изучить два примера:
JsonCodable
Это пилотный макрос от команды Dart, чтобы знакомиться с технологией. Советую разобрать его код. Почти всему я научился именно из него.
Args
Это мой макрос.
Если пишете консольные программы, то вы работали с аргументами командной строки. Обычно с ними работают с помощью стандартного пакета args:
import 'package:args/args.dart'; void main(List<String> argv) { final parser = ArgParser(); parser.addOption('name'); final results = parser.parse(argv); print('Hello, ' + results.option('name')); }
Если запустить
dart run main.dart --name=Alexey
То программа напечатает:
Hello, Alexey
Но если аргументов много, то это грязно. В них можно запутаться. Нет гарантии, что вы правильно напишете их в коде. Их трудно переименовывать, потому что это строковые литералы.
Я сделал макрос Args
, который оборачивает этот стандартный парсер и даёт гарантию безопасности типов:
import 'package:args_macro/args_macro.dart'; @Args() class HelloArgs { String name; int count = 1; } void main(List<String> argv) { final parser = HelloArgsParser(); // Сгенерированный класс парсера. final HelloArgs args = parser.parse(argv); for (int n = 0; n < args.count; n++) print('Hello, ${args.name}!'); }
Я подробно разберу этот пакет и как я его делал во второй части этой статьи. Подпишитесь, чтобы не пропустить её:
-
Мой канал в Telegram: ainkin_com
-
Мой русский канал (переводы выходят реже и с задержкой): ainkin_com_ru
-
Twitter: https://x.com/AlexeyInkin
ссылка на оригинал статьи https://habr.com/ru/articles/821911/
Добавить комментарий