在前几节课中,我们学习了混入(Mixin),掌握了面向对象编程中灵活复用代码的高级技巧。今天我们将探索 Dart 中另一个强大的特性 ------元数据与反射。它们让代码具备 "自我描述" 和 "动态分析" 的能力,在框架开发、代码生成、序列化等场景中发挥着重要作用。
一、元数据注解:给代码添加 "额外信息"
元数据(Metadata) 是嵌入在代码中,用于描述代码本身的额外信息。它不会直接影响代码的执行逻辑,但可以被编译器、工具或运行时环境读取,用于代码检查、生成或动态处理。
在 Dart 中,元数据通过注解(Annotation) 表示,以 @
符号开头,通常放在代码元素(类、方法、变量等)的前面。
1. 内置元数据注解
Dart 提供了几个常用的内置注解:
(1)@override
:标识重写父类方法
用于明确表示一个方法是重写父类的方法,帮助编译器检查是否正确重写(比如方法名、参数是否匹配):
dart
class Animal {
void makeSound() => print("动物叫声");
}
class Dog extends Animal {
// 明确标识重写父类方法
@override
void makeSound() => print("汪汪汪");
}
void main() {
Dog().makeSound(); // 输出:汪汪汪
}
如果不小心写错方法名(比如写成 makeSound2
),编译器会提示错误,避免低级错误。
(2)@deprecated
:标记过时的代码
用于标记不再推荐使用的代码,编译器会对使用该代码的地方发出警告:
dart
class Tool {
// 标记该方法已过时
@deprecated
void oldMethod() => print("这是旧方法,已过时");
void newMethod() => print("这是新方法,推荐使用");
}
void main() {
final tool = Tool();
tool.oldMethod(); // 编译时警告:'oldMethod' is deprecated
tool.newMethod(); // 无警告
}
通常会在注解后添加说明,告诉用户替代方案:
dart
@deprecated
/// 推荐使用 newMethod()
void oldMethod() => print("这是旧方法,已过时");
(3)@pragma
:向编译器传递特定指令
用于向 Dart 编译器传递平台相关的指令,比如禁用特定警告:
dart
// 禁用"未使用变量"的警告
@pragma('vm:unused')
int unusedVariable = 10;
void main() {
// 变量未使用,但不会触发警告
}
2. 元数据的本质:特殊的类实例
Dart 中所有注解本质上都是类的实例 ,@override
、@deprecated
等内置注解其实是预定义的类。我们可以通过源码理解:
dart
// 简化的 @deprecated 实现
class deprecated {
final String? message;
const deprecated([this.message]);
}
// 使用时,@deprecated 等价于 @deprecated()
@deprecated // 等同于 @deprecated()
void oldMethod() {}
这也是为什么注解可以带参数(比如 @deprecated("使用 newMethod 替代")
)。
二、自定义注解:创建自己的元数据
除了内置注解,我们还可以定义自己的注解,用于描述业务相关的信息(如序列化配置、权限校验等)。
1. 定义自定义注解
自定义注解的本质是创建一个带有 const 构造函数的类(因为注解需要在编译时确定值):
dart
// 定义"日志注解":标记需要打印日志的方法
class Logged {
final bool printParams; // 是否打印参数
final bool printResult; // 是否打印返回值
// 必须是 const 构造函数(注解在编译时解析)
const Logged({this.printParams = false, this.printResult = false});
}
// 定义"权限注解":标记需要权限校验的方法
class RequirePermission {
final String permission;
const RequirePermission(this.permission);
}
2. 使用自定义注解
将注解应用到类、方法、变量等元素上:
dart
class UserService {
// 使用 @Logged 注解,指定打印参数和返回值 @Logged参数和返回值
@Logged(printParams: true, printResult: true)
String getUserInfo(int id) {
return "用户 $id 的信息";
}
// 使用 @RequirePermission 注解,指定需要"admin"权限
@RequirePermission("admin")
void deleteUser(int id) {
print("删除用户 $id");
}
}
此时注解只是 "标记",还没有实际功能,需要配合注解处理器才能生效。
3. 编译时处理注解:生成代码
自定义注解的价值通常通过编译时代码生成实现:在代码编译阶段,通过工具读取注解信息,自动生成辅助代码(如序列化逻辑、日志代码等)。
以 json_serializable
包为例(它本质上是通过自定义注解 @JsonSerializable
实现自动生成 JSON 序列化代码):
- 定义模型类并添加注解:
dart
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // 生成的代码文件
@JsonSerializable() // 自定义注解
class User {
final String name;
final int age;
User({required this.name, required this.age});
// 生成的序列化方法
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
- 运行代码生成命令:
bash
dart run build_runner build
- 工具会读取
@JsonSerializable
注解,自动生成user.g.dart
文件,包含序列化逻辑。
这种 "编译时处理" 的优点是:不影响运行时性能(代码提前生成),支持 Flutter 等禁用反射的环境。
三、反射:让代码 "了解自身"
反射(Reflection) 是指程序在运行时动态获取自身结构(如类、方法、注解等信息)并操作的能力。简单说,就是代码可以 "观察" 和 "修改" 自己。
Dart 中反射主要通过 dart:mirrors
库实现,但该库在 Flutter 中被禁用(因为会增加包体积,影响性能)。实际开发中,更常用 reflectable
包(轻量级反射库,支持编译时配置)。
1. 反射的基本概念
反射可以实现以下功能:
- 获取类的名称、方法、属性等信息
- 动态创建类的实例
- 动态调用方法或修改属性值
- 读取注解信息并处理
2. 使用 reflectable
包实现反射
由于 dart:mirrors
在 Flutter 中不可用,我们以 reflectable
包为例演示反射功能。
(1)添加依赖
在 pubspec.yaml
中添加:
yaml
dependencies:
reflectable: ^4.0.13
dev_dependencies:
build_runner: ^2.4.4
(2)定义反射配置类
创建一个继承自 Reflectable
的类,指定需要反射的能力(如获取方法、读取注解等):
dart
// reflector.dart
import 'package:reflectable/reflectable.dart';
// 配置反射能力:支持实例化、调用方法、读取注解
class MyReflector extends Reflectable {
const MyReflector()
: super(
instanceInvokeCapability, // 允许调用实例方法
typeAnnotationQuantifyCapability, // 允许读取类型注解
declarationsCapability, // 允许获取类的声明(方法、属性等)
);
}
// 创建反射器实例
const myReflector = MyReflector();
(3)使用反射器标记需要反射的类
在需要反射的类上添加 @myReflector
注解:
dart
import 'reflector.dart';
// 用反射器标记该类,使其可被反射
@myReflector
class UserService {
@Logged(printParams: true, printResult: true)
String getUserInfo(int id) {
return "用户 $id 的信息";
}
@RequirePermission("admin")
void deleteUser(int id) {
print("删除用户 $id");
}
}
(4)生成反射代码
运行命令生成反射所需的辅助代码:
bash
dart run build_runner build
会生成 reflectable.g.dart
文件,包含反射所需的元数据。
(5)使用反射获取类信息并调用方法
dart
import 'package:reflectable/reflectable.dart';
import 'reflector.dart';
import 'reflectable.g.dart'; // 导入生成的代码
void main() {
// 初始化反射
initializeReflectable();
// 创建 UserService 实例
final userService = UserService();
// 获取类的反射信息
final instanceMirror = myReflector.reflect(userService);
final classMirror = instanceMirror.type;
// 打印类名
print("类名:${classMirror.simpleName}"); // 输出:类名:UserService
// 遍历类的方法
print("\n方法列表:");
for (var method in classMirror.declarations.values) {
if (method is MethodMirror && !method.isStatic) {
print("- ${method.simpleName}");
// 检查方法是否有 @Logged 注解
for (var annotation in method.annotations) {
if (annotation is Logged) {
print(" 有 @Logged 注解:printParams=${annotation.printParams}");
}
}
}
}
// 动态调用 getUserInfo 方法
print("\n动态调用方法:");
final result = instanceMirror.invoke("getUserInfo", [100]);
print("调用结果:$result"); // 输出:调用结果:用户 100 的信息
}
输出结果:
plaintext
类名:UserService
方法列表:
- getUserInfo
有 @Logged 注解:printParams=true
- deleteUser
动态调用方法:
调用结果:用户 100 的信息
3. 反射的慎用场景
尽管反射很强大,但在实际开发中(尤其是 Flutter 项目)应谨慎使用,原因如下:
- 性能损耗:反射需要在运行时解析类信息,比直接调用方法慢很多(通常慢 10-100 倍)。
- Flutter 不支持 :
dart:mirrors
库在 Flutter 中被禁用,reflectable
虽然可用,但会增加包体积。 - 代码可读性差:动态调用方法使代码逻辑更隐蔽,调试困难。
- 破坏类型安全:反射可以绕过编译器的类型检查,容易导致运行时错误。
替代方案 :优先使用编译时代码生成 (如 json_serializable
、freezed
),在编译阶段生成代码,兼顾灵活性和性能。
四、元数据与反射的实际应用场景
1. 序列化 / 反序列化
如 json_serializable
包,通过 @JsonSerializable
注解在编译时生成 JSON 转换代码,避免手动编写繁琐的序列化逻辑。
2. 依赖注入框架
如 get_it
等依赖注入框架,通过反射(或代码生成)自动创建对象并注入依赖,简化对象管理。
3. 路由管理
Flutter 路由框架(如 auto_route
)通过注解 @MaterialRoute
标记页面,编译时生成路由表,实现类型安全的路由跳转。
4. 日志与埋点
通过自定义注解(如 @LogEvent
)标记需要埋点的方法,结合编译时生成或反射,自动添加日志记录逻辑,减少重复代码。