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 {}
这一限制保证分析器和构建工具不必先运行应用,就能读取并处理元数据。
元数据可以标注什么
注解可以放在多种语言结构之前,包括类、扩展、字段、方法、函数、变量、参数和类型参数。也可以标注库相关指令,例如 import 和 export。
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() {}
元数据由谁读取
注解的使用通常分成两部分:
- 业务代码声明注解,并把它放到目标代码上。
- 分析器插件、构建器、代码生成器或框架读取注解,再执行检查或生成代码。
标准 Dart 语法只负责保存这些元数据,不规定每一种自定义注解的效果。某个注解是否能在运行时读取,也取决于所用平台、库和构建方式,不能仅凭添加 @Annotation() 就假定运行时反射可用。
TypeScript 对比:TypeScript 的装饰器也使用
@语法,但它们有自己的求值和调用规则,可能参与类及成员的运行时定义。Dart 注解首先是附加给声明的常量元数据,不会自行执行装饰逻辑,两者不能视为完全等价。
常见误区
误区一:认为注解会自动执行功能
@ApiRoute(...) 只提供信息。要让它真正注册路由,必须有框架或工具读取并处理该注解。
误区二:忘记声明常量构造函数
自定义注解类通常需要 const 构造函数。普通生成式构造函数创建的对象不是编译时常量,不能直接用作元数据。
误区三:把运行时变量传给注解
环境变量、当前时间、函数返回值等运行时数据无法作为注解参数。需要在程序运行后决定的配置,应通过普通对象或函数参数传递。
误区四:认为 @override 决定是否重写
即使省略 @override,签名匹配的成员仍然会重写父类型成员。保留该注解的价值在于表达意图,并让分析器帮助发现拼写或签名错误。
小结
- Dart 使用
@给声明和部分指令附加元数据。 @override用于声明重写意图,@Deprecated()用于提示 API 已弃用。- 自定义注解通常由带
const构造函数的类表示。 - 注解及其参数必须组成编译时常量,不能依赖运行时计算结果。
- 注解不会自动改变程序行为,需要分析器、生成器、框架或其他工具读取并处理。
- Dart 注解与 TypeScript 装饰器语法相似,但执行模型并不相同。