Escribimos nuestra propia macro en Dart 3.5 en lugar del antiguo generador de código / Sudo Null IT News
Dart 3.5 tiene una gran característica nueva: las macros. Es como la generación de código antigua, pero directamente en la memoria, sin archivos temporales, además de muchas más ventajas.
Esto todavía es una versión beta y hay poca documentación. Aquí plan Comandos de dardos:
Ahora han lanzado una macro.
@JsonCodable
que reemplaza el paquetejson_serializable
y elimina archivos .g.dart. Con su ejemplo, podrá familiarizarse con la tecnología.Esta macro se estabilizará durante 2024.
A principios de 2025 será posible escribir tus propias macros.
Pero resulta que ahora es posible: escribí y publiqué macro personalizadaY funciona: no es necesario esperar hasta 2025. Puedes hacer cualquier cosa, pero no lo uses en producción.
Entonces:
Veamos un ejemplo de uso.
@JsonCodable
del equipo Dardo.Escribamos nuestra macro más simple.
Echemos un vistazo profundo a mi macro, que genera un analizador de parámetros de línea de comando basado en la descripción de su clase de datos.
Preparando un experimento
Dardo 3.5
Descarga la versión beta de Dart 3.5 y habilita el uso de macros según las instrucciones oficiales: https://dart.dev/language/macros#set‑up‑the‑experiment
Acabo de descargar el archivo ZIP y lo puse en una carpeta separada.
Código VS
Para ver el código que producen las macros, necesita la última versión estable del complemento Dart para VSCode.
pubspec.yaml
Usar @JsonCodable
necesita la versión Dart 3.5.0-154
o mas alto. Establezca este requisito en pubspec.yaml:
name: macro_client
environment:
sdk: ^3.5.0-154
dependencies:
json: ^0.20.2
opciones_análisis.yaml
Para evitar que el analizador diga malas palabras, dígale que está experimentando con macros. Para hacer esto, cree el siguiente análisis_options.yaml:
analyzer:
enable-experiment:
- macros
El código
Copie el ejemplo oficial:
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());
}
Ejecute el programa con la bandera experimental en la terminal:
dart run --enable-experiment=macros lib/example.dart
O configure VSCode para que se ejecute de esta manera. Abra configuración.json:
E indique allí:
El ejemplo se ejecutará e imprimirá:
Instance of 'User'
{age: 5, name: Roger, username: roger1337}
Esto da como resultado solo 6 líneas en la clase:
@JsonCodable()
class User {
final int? age;
final String name;
final String username;
}
y con el paquete json_serializable
esto tomó 16 líneas:
@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);
}
Cómo ver el código generado
En VSCode, haga clic en el enlace “Ir a aumento” debajo de la línea donde se usa la macro @JsonCodable
. El código se abrirá:
A diferencia del antiguo generador, este no es un archivo en el disco: está todo en la memoria, por lo que no se puede editar.
Si cambia algo en el archivo fuente, el código generado reflejará inmediatamente los cambios; no es necesario ejecutar nada manualmente.
Y si VSCode no es adecuado para ti, escribí programaque muestra exactamente el mismo código generado.
Cómo funciona: Ogmentación
El código generado utiliza una nueva característica del lenguaje: aumento (“aumento”, traducido como “adición”). Esta es la capacidad de cambiar una clase o función agregando miembros y reemplazando cuerpos de funciones fuera del bloque donde se describieron originalmente.
Esta es una construcción sintáctica separada que no está relacionada con las macros. Aquí está el ejemplo más simple de su uso:
class Cat {
final String name; // Ошибка "Uninitialized", потому что нет конструктора.
}
augment class Cat {
Cat(this.name); // Исправляем эту ошибку.
}
La ogmentación también puede estar en un archivo separado. De hecho, la tarea principal de la macro es producir dicho archivo con omentación. Y la principal diferencia entre las macros y la generación anterior de código es que todo sucede en la memoria sin archivos temporales .g.dart en el disco.
Por tanto, en principio, sería posible rehacer el paquete. json_serializable
usando ogmentación e incluso sin macros, puede obtener el mismo código corto, porque el constructor se puede colocar en ogmentación y reenviadores de métodos. toJson
y fromJson
ya no es necesario.
Los principales honores son para las ogmentaciones, no para las macros. Sí, las macros son importantes, pero su papel es secundario en la revolución que pronto comenzará en torno al lenguaje.
Escribiendo nuestra propia macro de hola mundo
Crear hello.dart
con código de macro:
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.");}',
)),
);
}
}
Esta macro crea un método. hello
en cualquier clase a la que lo apliquemos. El método imprime el nombre de la clase y una lista de campos.
Una macro es una clase con un modificador. macro
. Aquí implementamos la interfaz. ClassDeclarationsMacro
, que le dice al compilador que esta macro es aplicable a clases y se ejecuta en la etapa en la que generamos declaraciones en ellas. Hay muchas interfaces que hacen que las macros sean aplicables a otras construcciones y les permiten funcionar en otras etapas de la generación de código. Hablaremos de esto cuando analicemos la macro para analizar los parámetros de la línea de comando.
La interfaz tiene un método. buildDeclarationsForClass
, que debe implementarse y se llamará automáticamente. Sus parámetros:
ClassDeclaration
con información sobre la clase a la que aplicamos la macro.Un constructor que puede analizar una declaración de clase y agregar código a la clase o globalmente a la biblioteca.
Usamos un constructor para obtener una lista de campos en una clase.
La generación de código real es fácil. El constructor tiene un método. declareInType
, que extiende la clase con cualquier código. En el caso más simple, puedes simplemente pasar una cadena, pero hay un truco con la función print
.
En el ejemplo con JsonCodable
Vimos arriba que la biblioteca dart:core
importado con el prefijo:
import 'dart:core' as prefix0;
El prefijo se agrega automáticamente para garantizar que su código no entre en conflicto con los símbolos de esta biblioteca. El prefijo es dinámico y no se puede conocer de antemano. Por lo tanto el desafío print(something)
No puedes escribirlo en código solo como una línea. Por lo tanto, generamos el código de partes:
final print = await builder.resolveIdentifier(_dartCore, 'print');
builder.declareInType(
DeclarationCode.fromParts(( // Части:
'void hello() {',
print,
'("Hello! I am ${clazz.identifier.name}. I have $fieldsString.");}',
)),
);
Estas partes pueden ser cadenas o referencias a identificadores obtenidos previamente. Al final, todo esto se pegará en una cadena y los identificadores recibirán los prefijos necesarios.
El código que utiliza nuestra nueva macro:
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();
}
Haga clic en “Ir a aumento” y vea el código resultante:
Tenga en cuenta que antes print
apareció un prefijo prefix0
según el cual la biblioteca dart:core
fue importado. Por cierto, la importación en sí se agregó como efecto secundario de incluir el identificador. print
en el código; no hicimos esta importación manualmente.
Correr:
dart run --enable-experiment=macros hello_client.dart
El programa imprimirá:
Hello! I am User. I have age, name, username.
Macros realmente útiles
Puedes estudiar dos ejemplos:
JsonCodable
Esta es una macro piloto del equipo de Dart para familiarizarse con la tecnología. Te aconsejo que lo desmontes. su codigo. Aprendí casi todo de él.
argumentos
Esta es mi macro.
Si escribe programas de consola, entonces ha trabajado con argumentos de línea de comando. Por lo general, se trabaja con ellos utilizando un paquete estándar. argumentos:
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'));
}
Si tu corres
dart run main.dart --name=Alexey
Luego el programa imprimirá:
Hello, Alexey
Pero si hay muchas discusiones, entonces es sucio. Puedes confundirte con ellos. No hay garantía de que los escriba correctamente en el código. Es difícil cambiarles el nombre porque son cadenas literales.
Hice macro Args
que envuelve este analizador estándar y proporciona una garantía de seguridad de tipos:
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}!');
}
Analizaré este paquete en detalle y cómo lo hice en la segunda parte de este artículo. Suscríbete para no perdértelo: