跳到主要内容

Metadata(元数据)

元数据用于给代码声明附加额外信息,供分析器、编译器、代码生成器或开发工具读取。Dart 使用 @ 在声明前添加注解(annotation);注解本身不会自动改变程序行为,真正如何处理它取决于读取该元数据的工具。

学习 Dart 元数据时,重点需要掌握以下问题:

  • 如何使用 @override@Deprecated() 等内置注解?
  • 如何定义并使用自定义注解?
  • 为什么注解必须是编译时常量?
  • 注解可以放在哪些代码位置?

使用内置注解

Dart 核心库提供了一些常用注解。@override 表示当前成员准备重写父类或接口中的成员:

class Greeter {
String greet() => 'Hello';
}

class DartGreeter extends Greeter {

String greet() => 'Hello, Dart';
}

void main() {
print(DartGreeter().greet()); // Hello, Dart
}

@override 可以让分析器检查重写关系。如果方法名或签名不再对应父类型成员,分析器就能报告问题。它不会在运行时创建新的重写机制;真正的重写仍由类的继承关系和方法声明决定。

标记已弃用的 API

@Deprecated() 表示某个 API 不建议继续使用,并可附带迁移提示:

('Use formatName() instead.')
String buildName(String name) => name;

String formatName(String name) => name.trim();

void main() {
print(formatName(' Dart ')); // Dart
}

调用被弃用的 buildName() 时,分析器会给出提示。弃用并不等于删除:原 API 仍可执行,因此维护者通常会先标记弃用,给调用方留出迁移时间,再在后续不兼容版本中删除。

Dart 也提供小写的 @deprecated 常量,但它不能携带说明。需要告诉调用方替代方案时,优先使用 @Deprecated('迁移说明')

定义自定义注解

注解可以引用编译时常量变量,也可以调用常量构造函数。实际项目通常定义一个带 const 构造函数的类,用字段保存元数据:

class Todo {
const Todo(this.owner, this.message);

final String owner;
final String message;
}

('team-data', 'Validate imported rows')
void importRows() {
print('Importing rows');
}

void main() {
importRows();
}

在元数据位置调用常量构造函数时,可以省略 const。因此 @Todo(...) 产生的是一个编译时常量 Todo 对象,而不是每次调用 importRows() 时重新创建的运行时对象。

注解类可以使用命名参数,让含义更清楚:

class ApiRoute {
const ApiRoute({required this.path, this.requiresAuth = true});

final String path;
final bool requiresAuth;
}

(path: '/health', requiresAuth: false)
String healthCheck() => 'ok';

这段代码只把路由信息附加到函数上。除非框架或自定义工具主动读取 ApiRoute,否则它不会自动注册 HTTP 路由。

注解参数必须是编译时常量

元数据必须能在编译阶段确定,因此注解构造函数需要是 const,传入的参数也必须是常量表达式:

class Label {
const Label(this.value);

final String value;
}

const stableLabel = 'stable';

(stableLabel)
class StableApi {}

运行时计算得到的值不能作为注解参数:

String createLabel() => 'generated';

// 编译错误:函数调用的结果不是编译时常量。
// @Label(createLabel())
// class GeneratedApi {}

这一限制保证分析器和构建工具不必先运行应用,就能读取并处理元数据。

元数据可以标注什么

注解可以放在多种语言结构之前,包括类、扩展、字段、方法、函数、变量、参数和类型参数。也可以标注库相关指令,例如 importexport

class RequiredInput {
const RequiredInput();
}

const requiredInput = RequiredInput();

class UserService {
('Use findById() instead.')
String find(String id) => id;

String findById( String id) => id;
}

同一个目标可以添加多个注解。它们分别提供信息,具体工具可以选择只处理自己认识的注解:

class Audit {
const Audit();
}

()
('Use saveSecurely() instead.')
void save() {}

元数据由谁读取

注解的使用通常分成两部分:

  1. 业务代码声明注解,并把它放到目标代码上。
  2. 分析器插件、构建器、代码生成器或框架读取注解,再执行检查或生成代码。

标准 Dart 语法只负责保存这些元数据,不规定每一种自定义注解的效果。某个注解是否能在运行时读取,也取决于所用平台、库和构建方式,不能仅凭添加 @Annotation() 就假定运行时反射可用。

TypeScript 对比:TypeScript 的装饰器也使用 @ 语法,但它们有自己的求值和调用规则,可能参与类及成员的运行时定义。Dart 注解首先是附加给声明的常量元数据,不会自行执行装饰逻辑,两者不能视为完全等价。

常见误区

误区一:认为注解会自动执行功能

@ApiRoute(...) 只提供信息。要让它真正注册路由,必须有框架或工具读取并处理该注解。

误区二:忘记声明常量构造函数

自定义注解类通常需要 const 构造函数。普通生成式构造函数创建的对象不是编译时常量,不能直接用作元数据。

误区三:把运行时变量传给注解

环境变量、当前时间、函数返回值等运行时数据无法作为注解参数。需要在程序运行后决定的配置,应通过普通对象或函数参数传递。

误区四:认为 @override 决定是否重写

即使省略 @override,签名匹配的成员仍然会重写父类型成员。保留该注解的价值在于表达意图,并让分析器帮助发现拼写或签名错误。

小结

  • Dart 使用 @ 给声明和部分指令附加元数据。
  • @override 用于声明重写意图,@Deprecated() 用于提示 API 已弃用。
  • 自定义注解通常由带 const 构造函数的类表示。
  • 注解及其参数必须组成编译时常量,不能依赖运行时计算结果。
  • 注解不会自动改变程序行为,需要分析器、生成器、框架或其他工具读取并处理。
  • Dart 注解与 TypeScript 装饰器语法相似,但执行模型并不相同。