dart学习第 19 节:元数据与反射 —— 代码的 “自我描述”

在前几节课中,我们学习了混入(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 序列化代码):

  1. 定义模型类并添加注解:
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);
}
  1. 运行代码生成命令:
bash 复制代码
dart run build_runner build
  1. 工具会读取 @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_serializablefreezed),在编译阶段生成代码,兼顾灵活性和性能。


四、元数据与反射的实际应用场景

1. 序列化 / 反序列化

json_serializable 包,通过 @JsonSerializable 注解在编译时生成 JSON 转换代码,避免手动编写繁琐的序列化逻辑。

2. 依赖注入框架

get_it 等依赖注入框架,通过反射(或代码生成)自动创建对象并注入依赖,简化对象管理。

3. 路由管理

Flutter 路由框架(如 auto_route)通过注解 @MaterialRoute 标记页面,编译时生成路由表,实现类型安全的路由跳转。

4. 日志与埋点

通过自定义注解(如 @LogEvent)标记需要埋点的方法,结合编译时生成或反射,自动添加日志记录逻辑,减少重复代码。

相关推荐
恋猫de小郭1 小时前
Android 禁止侧载将正式实施,需要等待 24 小时冷静期
android·flutter·harmonyos
FFF-X1 小时前
解决 Flutter Gradle 下载报错:修改默认 distributionUrl
flutter
程序员Ctrl喵21 小时前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难1 天前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡1 天前
flutter列表中实现置顶动画
flutter
始持1 天前
第十二讲 风格与主题统一
前端·flutter
始持1 天前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持1 天前
第十三讲 异步操作与异步构建
前端·flutter
新镜1 天前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴1 天前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter