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. @JsonCodableque reemplaza el paquete json_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:

  1. Veamos un ejemplo de uso. @JsonCodable del equipo Dardo.

  2. Escribamos nuestra macro más simple.

  3. 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 @JsonCodablenecesita 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:

  1. ClassDeclaration con información sobre la clase a la que aplicamos la macro.

  2. 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 prefix0segú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 Argsque 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:

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *