type
status
date
slug
summary
tags
category
icon
password
<ins/>
Dart 3.5 引入了一个重大新特性:宏。可以将其看作是在编译时完全内存中发生的代码生成,不需要临时文件。但它带来的远不止这些。
目前这一特性还处于测试阶段,Dart 团队谨慎地不透露过多信息,因为它还不稳定。他们的公开路线图如下:
- 目前他们有一个单一的
@JsonCodable
宏,替代了json_serializable
包,显著减少了其开销。我们可以通过这个宏来熟悉这个特性。
- 这个单一宏将在 2024 年某个时候稳定。
- 编写自己的宏将在 2025 年初提供。
但是,如果你现在就尝试创建自己的宏会怎样?他们的表述让我以为会有像启用宏的白名单之类的限制,但其实并没有!
我现在就可以创建并发布我自己的宏,并且它可以正常工作。我们不必等到 2025 年,也没有实验的限制,只是因为它目前还处于测试阶段,可能会出现问题,所以不建议在生产环境中使用。
让我们现在就来创建一些我们自己的宏吧!我们将会:
- 复现 Dart 团队的 hello-world 宏。
- 编写我们自己的 hello-world 宏。
- 深入探索我用于创建命令行参数解析器的宏。
设置实验环境
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
:并像下面这样进行修改:
现在它就能正常工作并打印以下内容:
注意,类
User
只有 6 行代码:而使用
json_serializable
的等效代码需要 16 行:查看生成的代码
在 VSCode 中,你可以点击 “Go to Augmentation” 来查看生成的代码:
与旧的代码生成方式不同,生成的代码并不是保存在真实的文件中,而是在内存中。你不能直接编辑这些代码,但是当你更改
main.dart
中的内容时,生成的代码会自动更新,所以你不需要单独运行生成器。如果你无法使用 VSCode,可以查看我提供的工具,它也可以显示相同的代码。
宏的工作原理:增强
那么这里发生了什么呢?这段代码使用了 Dart 新特性叫做 增强(augmentation)。增强是通过添加成员或替换类体外部的代码来修改类或函数。
该特性与宏是独立的,它最简单的用法如下:
这种增强可以位于与原始类不同的文件中。宏所做的实际操作是生成一个包含增强的文件。与旧的代码生成方式的实际区别在于,这个文件现在是内存中的,而不是一个
.g.dart
的物理文件。所以,如果 Dart 团队将
json_serializable
包升级为使用增强,那么你的代码可能会和使用宏时一样简洁,因为构造函数可以自动生成,你不再需要像 toJson
和 fromJson
这样的模板代码。真正值得称赞的杀手级特性是增强。虽然实现宏相对较为复杂,但它在编译器中的实现要难得多。
创建自己的 hello-world 宏
创建
hello.dart
文件,写入以下代码来实现宏:这个宏在你应用它的类上创建一个
hello
方法,该方法打印类的名字和它所拥有的字段。宏实现为一个带有
macro
修饰符的类,它实现了 ClassDeclarationsMacro
,这告诉编译器它可以应用于类,并且会在适当的时候更新声明。这个接口有一个 buildDeclarationsForClass
方法,我们需要实现它,它会在合适的时机被调用,并且接收以下参数:- 类的声明,用来访问该类的信息。
builder
对象,提供方法来检查给定的类并向其添加代码。
我们使用
builder
获取类的字段。实际的代码生成非常简单。
builder
提供了 declareInType
方法来向正在增强的类中添加任何代码。最简单的代码可以只是一个字符串,但有一个 tricky 部分,因为你不能仅仅将 print
函数当作字符串写进去。如果你查看 Dart 团队提供的
JsonCodable
宏,你会发现它在引入 dart:core
时会带上一个前缀:这个前缀是动态的,你不能提前知道,所以你不能直接在生成的代码中写
print(something)
。这就是为什么我们需要通过 builder.resolveIdentifier
来解析 print
函数,并通过 DeclarationCode.fromParts
将生成的代码分解为 部分 :parts
可以是字符串和标识符的混合,最终会将它们拼接起来。所有标识符都会加上必要的前缀。写下如下代码来使用这个新宏:
点击 “Go to Augmentation” 查看生成的代码:
注意,
print
函数前面加上了 prefix0
,这是引入 dart:core
时的标识符前缀。这一切都是自动完成的。请注意,我们并没有显式地添加 import 'dart:core'
。运行代码:
你将看到它输出:
真实有用的宏
以下是两个可以深入学习的实际宏:
JsonCodable
这是 Dart 团队发布的第一个宏,帮助我们了解这一特性。我强烈建议阅读它的代码。这是我学到几乎所有知识的地方。
Args
这是我创建的宏。
如果你开发的是命令行应用,应该熟悉命令行参数及其解析。通常会使用标准的args 包:
运行:
输出:
但是,当命令行选项很多时,这样的代码就变得很混乱。你可能会弄错选项的名称,而且无法在编译时保证某个选项是否存在以及其类型是否正确。重命名选项也非常困难,因为这段代码使用的是字符串字面量来操作选项名。
所以我创建了Args宏,它从任何数据类生成解析器,并且在编译时提供类型安全:
这段代码运行时,它会自动生成一个
HelloArgsParser
类来处理命令行参数,你可以保证参数在编译时就被验证类型。<ins/>