跳转到正文
YGJoon 技术博客

Dart 元编程进阶

发布于:
·1.7k 字 · 约 4 分钟·作者:
进阶玩法:运行时类型操作与动态调用、build.yaml 配置、自定义构建器与注解处理器,把重复代码交给机器去写。
Dart 元编程进阶文章封面
目录

反射 API 高级应用

注意:dart:mirrors 只能跑在 Dart VM 上(命令行脚本、服务端),Flutter App 内无法使用反射——做跨平台项目时要提前想好替代方案。

1. 运行时类型操作

import 'dart:mirrors';

void main() {
  final mirror = reflect(MyClass());
  final classMirror = mirror.type;
  
  // 获取类的元数据
  print('类名: ${classMirror.simpleName}');
  print('是否为抽象类: ${classMirror.isAbstract}');
  
  // 获取所有方法
  classMirror.declarations.forEach((symbol, declarationMirror) {
    if (declarationMirror is MethodMirror && !declarationMirror.isConstructor) {
      print('方法: ${symbol}');
      print('返回类型: ${declarationMirror.returnType.reflectedType}');
    }
  });
}

class MyClass {
  String getName() => 'MyClass';
  int calculate(int x) => x * 2;
}

2. 动态调用方法

import 'dart:mirrors';

void main() {
  final instance = MyService();
  final mirror = reflect(instance);
  
  // 动态调用方法
  final result = mirror.invoke(
    #processData,
    ['test data'],
    {#option: true},
  );
  
  print('调用结果: ${result.reflectee}');
}

class MyService {
  String processData(String data, {bool option = false}) {
    return option ? data.toUpperCase() : data.toLowerCase();
  }
}

代码生成工具链

1. 构建器配置

## build.yaml
targets:
  $default:
    builders:
      my_builder:
        enabled: true
        generate_for:
          - lib/**/*.dart

2. 自定义构建器

import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:analyzer/dart/element/element.dart';

class MyBuilder extends Builder {
  @override
  Map<String, List<String>> get buildExtensions => {
    '.dart': ['.g.dart'],
  };
  
  @override
  Future<void> build(BuildStep buildStep) async {
    final inputId = buildStep.inputId;
    final content = await buildStep.readAsString(inputId);
    
    // 生成代码逻辑
    final generatedCode = generateCode(content);
    
    // 写入生成的文件
    final outputId = inputId.changeExtension('.g.dart');
    await buildStep.writeAsString(outputId, generatedCode);
  }
}

String generateCode(String source) {
  // 实现代码生成逻辑
  return '''// 生成的代码
// ...
''';
}

3. 注解处理器

// 定义注解
class GenerateJson {
  const GenerateJson();
}

// 注解处理器
class JsonGenerator extends GeneratorForAnnotation<GenerateJson> {
  @override
  String generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    if (element is! ClassElement) {
      throw InvalidGenerationSourceError(
        '只能在类上使用 @GenerateJson 注解',
        element: element,
      );
    }
    
    return generateJsonCode(element);
  }
}

String generateJsonCode(ClassElement classElement) {
  final className = classElement.name;
  final buffer = StringBuffer();

  // 生成可从类外调用的顶层函数,并把实例显式作为参数传入。
  buffer.writeln(
    "Map<String, dynamic> _\$${className}ToJson($className instance) => {",
  );
  for (final field in classElement.fields) {
    if (field.isStatic || field.isPrivate || field.isSynthetic) continue;
    buffer.writeln("  '${field.name}': instance.${field.name},");
  }
  buffer.writeln('};');
  return buffer.toString();
}

这个片段只演示“生成的顶层函数通过 instance 访问公开实例字段”的作用域关系。它没有覆盖重命名、继承、私有字段、默认值、泛型和类型转换;真实 JSON 生成器需要逐项定义这些规则,并在 generator 工程中分析生成文件。

元数据处理

1. 自定义注解

// 定义注解
class Validate {
  final String pattern;
  final String message;
  
  const Validate(this.pattern, {this.message = ''});
}

// 使用注解
class User {
  @Validate(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$', message: '无效的邮箱格式')
  String email;
  
  @Validate(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$', message: '密码至少8位,包含字母和数字')
  String password;
  
  User(this.email, this.password);
}

2. 注解处理

import 'dart:mirrors';

class ValidationProcessor {
  static List<String> validate(Object instance) {
    final errors = <String>[];
    final mirror = reflect(instance);
    final classMirror = mirror.type;
    
    classMirror.declarations.forEach((symbol, declarationMirror) {
      if (declarationMirror is VariableMirror) {
        final annotations = declarationMirror.metadata;
        for (final annotation in annotations) {
          if (annotation.reflectee is Validate) {
            final validate = annotation.reflectee as Validate;
            final value = mirror.getField(symbol).reflectee as String;
            
            if (!RegExp(validate.pattern).hasMatch(value)) {
              errors.add(validate.message.isNotEmpty
                ? validate.message
                : '${symbol} 验证失败');
            }
          }
        }
      }
    });
    
    return errors;
  }
}

代码生成最佳实践

1. 增量构建与缓存

build_runner 管理构建步骤和增量输入;不要在 Builder 内遇到缓存命中就直接 return 而不写输出文件,否则当前构建可能缺少生成产物。缓存行为应遵循 build 系统的输入/输出模型,并用连续两次构建验证结果一致。

2. 错误处理

class ErrorHandlingBuilder extends Builder {
  @override
  Map<String, List<String>> get buildExtensions => {
    '.dart': ['.g.dart'],
  };

  @override
  Future<void> build(BuildStep buildStep) async {
    try {
      // 构建逻辑
    } catch (e, stackTrace) {
      // 注意:package:build 里没有顶层 log,这里直接打印,信息里带上文件路径方便定位
      print('构建失败: $e\n文件: ${buildStep.inputId.path}\n$stackTrace');
      // 提供有用的错误信息
      throw BuilderError(
        '构建失败: ${e.toString()}\n'
        '文件: ${buildStep.inputId.path}\n'
        '请检查源代码或报告问题',
      );
    }
  }
}

class BuilderError extends Error {
  final String message;
  
  BuilderError(this.message);
  
  @override
  String toString() => message;
}

调试技巧

1. 日志记录

class DebugBuilder extends Builder {
  static const _debug = true;

  @override
  Map<String, List<String>> get buildExtensions => {
    '.dart': ['.g.dart'],
  };

  void debugLog(String message) {
    if (_debug) {
      print('[DEBUG] $message');
    }
  }
  
  @override
  Future<void> build(BuildStep buildStep) async {
    debugLog('开始处理: ${buildStep.inputId.path}');
    // 构建逻辑
    debugLog('处理完成');
  }
}

2. 代码检查

代码片段的语法检查和 lint 是两件事。对于真正的 generator 工程,应把生成产物写到 build 输出中,再运行 dart analyze 或 generator 项目配置的分析步骤;不要用一个未配置规则的 Linter([]) 代替完整分析。

实战案例:我绕开了整个元编程工具链

本文讲的都是”正统”元编程:build_runner、source_gen、注解处理器。但我的 ygking_json_to_dart_model 项目(143 行,零第三方代码生成依赖)走的是另一条路——运行时字符串模板。

对比一下两种路线:

build_runner 路线我的字符串模板路线
时机编译期运行期(用户点”生成”按钮时)
输入已写好的 Dart 类 + 注解一段 JSON 文本
输出.g.dart 文件直接显示在预览区的字符串
依赖build_runner + source_gen + 注解包零依赖,dart:convert 就够

选字符串模板不是因为不懂 build_runner,而是场景根本套不上:build_runner 是给”工程代码”做生成的,我的场景是”用户扔一段 JSON 进来,立刻要一份 Dart 类”。为这个去配 build.yaml、写 Builder 子类,是大炮打蚊子。

但字符串模板有个硬伤我得承认:零类型检查。buffer.writeln(' $type? $key;') 拼出来的代码,Dart 编译器在”生成”那一刻是看不见的——拼错了要等用户把结果复制到工程里编译才发现。所以我定了条死规矩:生成器自己的单元测试必须覆盖”生成的代码能通过 dart analyze”(项目里有 test/ 目录就是干这个的)。

一句话总结:元编程的本质是”让程序写程序”,build_runner 只是其中一种实现。当你的”程序”简单到 143 行字符串拼接就能搞定时,别为了”正统”而上工具链。

总结

Dart 元编程包含运行时反射、静态代码分析、注解和构建期代码生成等不同机制。选择方案时要先确认运行平台、代码生成发生的时机,以及生成物能否被分析和测试。

真实 generator 工程应固定 Dart、analyzer、build_runner 与 source_gen 版本;示例片段不能代替完整工程的 dart analyze 和生成物检查。

参考资料


分享这篇文章:


相关文章

上一篇
Dart 并发编程进阶