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)标记需要埋点的方法,结合编译时生成或反射,自动添加日志记录逻辑,减少重复代码。

相关推荐
叽哥1 小时前
dart学习第 20 节:错误处理与日志 —— 让程序更健壮
flutter·dart
TralyFang2 小时前
flutter key:ValueKey、ObjectKey、UniqueKey、GlobalKey的使用场景
flutter
叽哥3 小时前
dart学习第 15 节:Stream—— 处理连续数据流
flutter·dart
w_y_fan4 小时前
Flutter中蓝牙开发:flutter_blue_plus的应用理解
flutter
LZQ <=小氣鬼=>4 小时前
Flutter简单讲解
flutter
来来走走6 小时前
Flutter开发 StatelessWidget与StatefulWidget基本了解
android·flutter
LZQ <=小氣鬼=>7 小时前
Flutter 事件总线 Event Bus
flutter·dart·事件总线·event bus
天岚10 小时前
温故知新-SchedulerBinding
flutter
0wioiw012 小时前
Apple基础(Xcode⑤-Flutter-Singbox-AI提示词)
flutter·macos·xcode