type
status
date
slug
summary
tags
category
icon
password
<ins/>
Dart 3.5 引入了一个重大新特性:宏。可以将其看作是在编译时完全内存中发生的代码生成,不需要临时文件。但它带来的远不止这些。
目前这一特性还处于测试阶段,Dart 团队谨慎地不透露过多信息,因为它还不稳定。他们的公开路线图如下:
  • 目前他们有一个单一的 @JsonCodable 宏,替代了 json_serializable 包,显著减少了其开销。我们可以通过这个宏来熟悉这个特性。
  • 这个单一宏将在 2024 年某个时候稳定。
  • 编写自己的宏将在 2025 年初提供。
但是,如果你现在就尝试创建自己的宏会怎样?他们的表述让我以为会有像启用宏的白名单之类的限制,但其实并没有!
我现在就可以创建并发布我自己的宏,并且它可以正常工作。我们不必等到 2025 年,也没有实验的限制,只是因为它目前还处于测试阶段,可能会出现问题,所以不建议在生产环境中使用。
让我们现在就来创建一些我们自己的宏吧!我们将会:
  1. 复现 Dart 团队的 hello-world 宏。
  1. 编写我们自己的 hello-world 宏。
  1. 深入探索我用于创建命令行参数解析器的宏。

设置实验环境

Dart 3.5

按照官方指示切换到 Dart 3.5 测试版:https://dart.dev/language/macros#set-up-the-experiment
我直接下载了 ZIP 文件并将其解压到一个独立的路径。

VSCode

需要安装最新的 Dart 插件来查看宏生成的代码。

pubspec.yaml

要使用示例宏,必须至少使用 Dart 3.5.0-154。创建如下的 pubspec.yaml 文件:

analysis_options.yaml

由于你正在编写代码时使用的是实验特性,分析器会给出警告,因此需要告诉它你正在实验这个特性。创建以下的 analysis_options.yaml 文件:

编写代码

使用 Dart 团队提供的示例代码:
通过终端运行代码,启用实验标志:
或者配置 VSCode 来进行实验。打开 settings.json
notion image
并像下面这样进行修改:
notion image
现在它就能正常工作并打印以下内容:
注意,类 User 只有 6 行代码:
而使用 json_serializable 的等效代码需要 16 行:

查看生成的代码

在 VSCode 中,你可以点击 “Go to Augmentation” 来查看生成的代码:
notion image
与旧的代码生成方式不同,生成的代码并不是保存在真实的文件中,而是在内存中。你不能直接编辑这些代码,但是当你更改 main.dart 中的内容时,生成的代码会自动更新,所以你不需要单独运行生成器。
如果你无法使用 VSCode,可以查看我提供的工具,它也可以显示相同的代码。

宏的工作原理:增强

那么这里发生了什么呢?这段代码使用了 Dart 新特性叫做 增强(augmentation)。增强是通过添加成员或替换类体外部的代码来修改类或函数。
该特性与宏是独立的,它最简单的用法如下:
这种增强可以位于与原始类不同的文件中。宏所做的实际操作是生成一个包含增强的文件。与旧的代码生成方式的实际区别在于,这个文件现在是内存中的,而不是一个 .g.dart 的物理文件。
所以,如果 Dart 团队将 json_serializable 包升级为使用增强,那么你的代码可能会和使用宏时一样简洁,因为构造函数可以自动生成,你不再需要像 toJsonfromJson 这样的模板代码。
真正值得称赞的杀手级特性是增强。虽然实现宏相对较为复杂,但它在编译器中的实现要难得多。

创建自己的 hello-world 宏

创建 hello.dart 文件,写入以下代码来实现宏:
这个宏在你应用它的类上创建一个 hello 方法,该方法打印类的名字和它所拥有的字段。
宏实现为一个带有 macro 修饰符的类,它实现了 ClassDeclarationsMacro,这告诉编译器它可以应用于类,并且会在适当的时候更新声明。这个接口有一个 buildDeclarationsForClass 方法,我们需要实现它,它会在合适的时机被调用,并且接收以下参数:
  1. 类的声明,用来访问该类的信息。
  1. builder 对象,提供方法来检查给定的类并向其添加代码。
我们使用 builder 获取类的字段。
实际的代码生成非常简单。builder 提供了 declareInType 方法来向正在增强的类中添加任何代码。最简单的代码可以只是一个字符串,但有一个 tricky 部分,因为你不能仅仅将 print 函数当作字符串写进去。
如果你查看 Dart 团队提供的 JsonCodable 宏,你会发现它在引入 dart:core 时会带上一个前缀:
这个前缀是动态的,你不能提前知道,所以你不能直接在生成的代码中写 print(something)。这就是为什么我们需要通过 builder.resolveIdentifier 来解析 print 函数,并通过 DeclarationCode.fromParts 将生成的代码分解为 部分
parts 可以是字符串和标识符的混合,最终会将它们拼接起来。所有标识符都会加上必要的前缀。
写下如下代码来使用这个新宏:
点击 “Go to Augmentation” 查看生成的代码:
notion image
注意,print 函数前面加上了 prefix0,这是引入 dart:core 时的标识符前缀。这一切都是自动完成的。请注意,我们并没有显式地添加 import 'dart:core'
运行代码:
你将看到它输出:

真实有用的宏

以下是两个可以深入学习的实际宏:

JsonCodable

这是 Dart 团队发布的第一个宏,帮助我们了解这一特性。我强烈建议阅读它的代码。这是我学到几乎所有知识的地方。

Args

这是我创建的宏。
如果你开发的是命令行应用,应该熟悉命令行参数及其解析。通常会使用标准的args 包:
运行:
输出:
但是,当命令行选项很多时,这样的代码就变得很混乱。你可能会弄错选项的名称,而且无法在编译时保证某个选项是否存在以及其类型是否正确。重命名选项也非常困难,因为这段代码使用的是字符串字面量来操作选项名。
所以我创建了Args宏,它从任何数据类生成解析器,并且在编译时提供类型安全:
这段代码运行时,它会自动生成一个 HelloArgsParser 类来处理命令行参数,你可以保证参数在编译时就被验证类型。
<ins/>