跳转到正文
YGJoon 技术博客

Dart Mixin 和扩展详解

发布于:
·1.2k 字 · 约 3 分钟·作者:
讲透 Mixin 的声明、on 约束与多混入组合,再教你用扩展方法给现有类加能力:不改源码也能增强 String、List 的实战技巧。
Dart Mixin 和扩展详解文章封面
目录

Mixin 基础

Mixin 声明

mixin Logger {
  void log(String message) {
    print('Log: $message');
  }
  
  void error(String message) {
    print('Error: $message');
  }
}

mixin TimeStamp {
  String getTimestamp() {
    return DateTime.now().toIso8601String();
  }
}

Mixin 使用

class Service with Logger, TimeStamp {
  void doSomething() {
    log('Operation started at ${getTimestamp()}');
    // 执行操作
    log('Operation completed');
  }
}

Mixin 约束

on 关键字

// 限制只能在特定类型上使用
mixin CanFly on Bird {
  void fly() {
    print('Flying high!');
  }
}

class Bird {
  void chirp() {
    print('Chirp chirp!');
  }
}

class Eagle extends Bird with CanFly {
  // 可以使用 fly 方法
}

class Dog with CanFly {  // 错误: Dog 不是 Bird 的子类
  // 编译错误
}

多个 Mixin

mixin A {
  String getMessage() => 'A';
}

mixin B {
  String getMessage() => 'B';
}

class C with A, B {
  // 使用最后一个 mixin 的实现
  // getMessage() 返回 'B'
}

扩展方法

基本语法

extension StringExtension on String {
  bool isValidEmail() {
    return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
  }
  
  String capitalize() {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1)}';
  }
}

// 使用扩展方法
void main() {
  print('[email protected]'.isValidEmail()); // true
  print('hello'.capitalize()); // Hello
}

泛型扩展

extension ListExtension<T> on List<T> {
  T? firstWhereOrNull(bool Function(T) test) {
    for (var element in this) {
      if (test(element)) return element;
    }
    return null;
  }
  
  List<T> sortedBy(Comparable Function(T) keyExtractor) {
    var copy = [...this];
    copy.sort((a, b) => keyExtractor(a).compareTo(keyExtractor(b)));
    return copy;
  }
}

完整示例

// 基础日志 mixin
mixin BaseLogger {
  void log(String message) {
    print('${DateTime.now()}: $message');
  }
}

// 文件操作 mixin:用到了 BaseLogger 的 log,所以用 on 声明依赖
mixin FileOperations on BaseLogger {
  Future<void> writeToFile(String path, String content) async {
    log('Writing to file: $path'); // 可以使用 BaseLogger 的方法
    // 文件写入逻辑
  }
  
  Future<String> readFromFile(String path) async {
    log('Reading from file: $path');
    // 文件读取逻辑
    return 'File content';
  }
}

// 数据验证 mixin
mixin DataValidator {
  bool validateEmail(String email) {
    return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
  }
  
  bool validatePhone(String phone) {
    return RegExp(r'^\+?[\d\s-]{10,}$').hasMatch(phone);
  }
}

// 使用多个 mixin
class UserService with BaseLogger, FileOperations, DataValidator {
  Future<void> saveUser(Map<String, String> userData) async {
    // 验证数据
    if (!validateEmail(userData['email']!)) {
      log('Invalid email address');
      return;
    }
    
    if (!validatePhone(userData['phone']!)) {
      log('Invalid phone number');
      return;
    }
    
    // 保存数据
    await writeToFile('users.json', userData.toString());
    log('User data saved successfully');
  }
}

// 扩展方法示例
extension MapExtension<K, V> on Map<K, V> {
  Map<K, V> merge(Map<K, V> other) {
    return {...this, ...other};
  }
  
  // 按 key 路径逐层取值,取不到返回 null;叶子值类型不确定,所以返回 dynamic
  dynamic getPath(List<K> path) {
    dynamic current = this;
    for (final key in path) {
      if (current is Map) {
        current = current[key];
      } else {
        return null;
      }
    }
    return current;
  }
}

void main() async {
  // 使用 UserService
  final userService = UserService();
  await userService.saveUser({
    'email': '[email protected]',
    'phone': '+1234567890',
  });
  
  // 使用扩展方法
  final map1 = {'a': 1, 'b': 2};
  final map2 = {'c': 3, 'd': 4};
  final merged = map1.merge(map2);
  print(merged); // {a: 1, b: 2, c: 3, d: 4}
  
  final nested = {
    'user': {
      'address': {
        'city': 'New York'
      }
    }
  };
  print(nested.getPath(['user', 'address', 'city'])); // New York
}

高级用法

条件 Mixin

mixin DebugLogger on Object {
  void debugLog(String message) {
    assert(() {
      print('Debug: $message');
      return true;
    }());
  }
}

扩展运算符

extension IterableExtension<T> on Iterable<T> {
  T? get firstOrNull => isEmpty ? null : first;
  
  Map<K, List<T>> groupBy<K>(K Function(T) keyExtractor) {
    var result = <K, List<T>>{};
    for (var element in this) {
      var key = keyExtractor(element);
      (result[key] ??= []).add(element);
    }
    return result;
  }
}

最佳实践

  1. 合理使用 Mixin 组合功能
  2. 避免 Mixin 之间的冲突
  3. 为扩展方法提供文档
  4. 使用有意义的命名
  5. 注意扩展方法的作用域

注意事项

  1. Mixin 不支持构造函数
  2. 注意 Mixin 的顺序
  3. 避免过度使用扩展方法
  4. 处理好命名冲突
  5. 考虑扩展方法的性能影响

实战案例:一个真实在用的 capitalize()

本文 84 行那个 StringExtension 的例子不是我编的——我的 ygking_json_to_dart_model 项目里真有这么一行:

// lib/services/json_converter.dart:嵌套 JSON 对象生成嵌套类时
$key!.add(${key.capitalize()}.fromJson(v));

场景是:JSON 里 "user": {...},我要生成一个类名叫 User 的 Dart 类。JSON 的 key 是小驼峰,Dart 类名要大驼峰,key.capitalize() 就是干这个的。

为什么用扩展方法而不用普通函数?对比一下:

// 普通函数写法
add(capitalize(key).fromJson(v));
// 扩展方法写法
add(key.capitalize().fromJson(v));

扩展方法的阅读顺序是从左到右的链式:key → 首字母大写 → fromJson。普通函数是嵌套的,要从里往外读。在生成器这种”一句话里做好几件事”的代码里,扩展方法的链式写法可读性明显更好。

但我也给自己定了条规矩:扩展方法只给”通用小操作”用(字符串处理、集合判空这种),业务逻辑一律写成普通方法。原因很实际——扩展方法是全局生效的,String 上挂个 capitalize() 没人会奇怪;但如果在 String 上挂个 toOrderModel(),三个月后你自己都忘了它藏在哪。这条线守住了,扩展方法才是利器,守不住就是灾难。

总结

Mixin 解决”多个类共享一段行为”,扩展方法解决”不改源码给现有类加能力”。两个都别滥用:mixin 记得用 on 写清依赖,扩展方法只收通用小操作——具体分寸看上面的实战案例。


分享这篇文章:


相关文章

上一篇
Dart 泛型详解
下一篇
Dart Stream 和 Future 详解