[Перевод] Пишем собственный макрос на Dart 3.5 вместо старого генератора кода
В 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 map) => _$UserFromJson(map);
Map 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 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 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 argv) {
final parser = HelloArgsParser(); // Сгенерированный класс парсера.
final HelloArgs args = parser.parse(argv);
for (int n = 0; n < args.count; n++)
print('Hello, ${args.name}!');
}
Я подробно разберу этот пакет и как я его делал во второй части этой статьи. Подпишитесь, чтобы не пропустить её: