dart学习第 18 节:面向对象高级 —— 混入(Mixin)

82 阅读6分钟

在前几节课中,我们学习了文件操作和命令行工具开发,掌握了处理实际开发任务的实用技能。今天我们将深入探讨 Dart 面向对象编程中的一个高级特性 ——混入(Mixin) 。它是 Dart 中解决代码复用和多继承问题的独特方案,能够让我们更灵活地组织和扩展类的功能。

一、为什么需要 Mixin?—— 解决 “多继承” 困境

在传统面向对象编程中,单继承机制(一个类只能有一个父类)限制了代码复用的灵活性,而多继承(一个类可以有多个父类)又会带来 “菱形问题”(多个父类中出现同名方法时的歧义)。

Mixin 正是为解决这一矛盾而生:

  • 它允许一个类 “混入” 多个功能模块,实现类似多继承的效果
  • 但又通过严格的规则避免了多继承的歧义问题
  • 专注于功能复用而非类的层次关系

举个生活例子:

  • 鸟类继承自动物类(单继承)
  • 但 “会飞”、“会游泳” 是独立的功能,不应作为继承关系
  • 可以通过 Mixin 给不同鸟类动态添加 “飞翔” 或 “游泳” 功能(如鸭子会游泳,麻雀会飞)

代码对比:继承 vs Mixin

// 传统继承方式的局限
class Animal {
  void eat() => print("吃东西");
}

// 正确定义Mixin(使用mixin关键字)
mixin Flyable {
  void fly() => print("会飞");
}

mixin Swimmable {
  void swim() => print("会游泳");
}

// 使用Mixin解决:通过with关键字混入多个功能
class Duck extends Animal with Flyable, Swimmable {
  // 同时拥有 eat()、fly()、swim() 方法
}

void main() {
  final duck = Duck();
  duck.eat(); // 输出:吃东西
  duck.fly(); // 输出:会飞
  duck.swim(); // 输出:会游泳
}

注意:在 Dart 中,只有用 mixin 关键字定义的类才能作为混入使用,普通类不能直接用作 Mixin,否则会报 “The class can't be used as a mixin” 错误。



二、Mixin 的定义与基本使用

Mixin 本质上是一种特殊的类,它不能被实例化,专门用于给其他类添加功能。

1. 定义 Mixin

使用 mixin 关键字定义一个混入:

// 定义一个日志功能的Mixin
mixin Loggable {
  // 混入中可以包含方法和属性
  void log(String message) {
    final time = DateTime.now().toIso8601String();
    print("[$time] $message");
  }
}

// 定义一个缓存功能的Mixin
mixin Cacheable {
  final Map<String, dynamic> _cache = {};

  void cache(String key, dynamic value) {
    _cache[key] = value;
    print("缓存 $key: $value");
  }

  dynamic getCache(String key) => _cache[key];
}

2. 使用 Mixin:with 关键字

通过 with 关键字给类 “混入” 功能:

// 普通类
class UserService {
  // 基础功能:获取用户信息
  String getUserInfo(int id) {
    return "用户 $id 的信息";
  }
}

// 混入功能:添加日志和缓存能力
class EnhancedUserService extends UserService with Loggable, Cacheable {
  // 重写父类方法,添加日志和缓存
  @override
  String getUserInfo(int id) {
    // 使用Loggable的方法
    log("开始获取用户 $id 的信息");

    // 先查缓存
    final cached = getCache(id.toString());
    if (cached != null) {
      log("从缓存获取用户 $id 的信息");
      return cached;
    }

    // 实际获取(调用父类方法)
    final info = super.getUserInfo(id);

    // 存入缓存
    cache(id.toString(), info);
    log("获取用户 $id 的信息完成");

    return info;
  }
}

void main() {
  final service = EnhancedUserService();

  // 第一次获取(无缓存)
  print(service.getUserInfo(100));

  // 第二次获取(有缓存)
  print(service.getUserInfo(100));
}

// 输出:
// [2023-10-20T10:00:00.000] 开始获取用户 100 的信息
// 缓存 100: 用户 100 的信息
// [2023-10-20T10:00:00.001] 获取用户 100 的信息完成
// 用户 100 的信息
// [2023-10-20T10:00:00.002] 开始获取用户 100 的信息
// [2023-10-20T10:00:00.002] 从缓存获取用户 100 的信息
// 用户 100 的信息

3. Mixin 的规则与限制

  • 不能实例化:Mixin 是为了被混入而设计的,不能直接创建对象
// 错误!不能实例化Mixin
// final log = Loggable();

可以限制使用范围:通过 on 关键字指定该 Mixin 只能被特定类的子类使用

class Base {
  void basic() => print("基础方法");
}

mixin Advanced on Base {
  // 限制只能混入Base的子类
  void advanced() {
    basic(); // 可以调用Base类的方法
    print("高级方法");
  }
}

class MyClass extends Base with Advanced {
  // 正确:先继承Base,再混入Advanced
}

// 错误:OtherClass不继承Base,不能混入Advanced
// class OtherClass with Advanced {}

with 后面的顺序很重要:当多个 Mixin 有同名方法时,后面的会覆盖前面的

mixin A {
  void method() => print("A");
}

mixin B {
  void method() => print("B");
}

class C with A, B {} // 后面的B覆盖A

class D with B, A {} // 后面的A覆盖B

void main() {
  C().method(); // 输出:B
  D().method(); // 输出:A
}

Dart 2.17+ 中的 mixin class:如果需要一个既能作为普通类又能作为 Mixin 的类,可以使用 mixin class

mixin class Flyable {
  void fly() => print("会飞");
}

// 既可以作为普通类继承
class Bird extends Flyable {}

// 也可以作为Mixin混入
class Insect with Flyable {}


三、场景实战:用 Mixin 给类动态添加功能

Mixin 最适合的场景是给不同类添加通用功能,如日志、缓存、序列化等。

1. 日志功能 Mixin

为任何类添加自动日志记录能力:

mixin Loggable {
  // 记录方法调用日志
  void logMethodCall(String methodName, [List<dynamic> args = const []]) {
    final argsStr = args.join(', ');
    print("[调用方法] $methodName($argsStr)");
  }

  // 记录方法返回日志
  void logMethodReturn(String methodName, dynamic result) {
    print("[返回结果] $methodName: $result");
  }
}

// 应用到数据处理类
class DataProcessor with Loggable {
  int process(int a, int b) {
    logMethodCall("process", [a, b]);
    final result = a * 2 + b;
    logMethodReturn("process", result);
    return result;
  }
}

void main() {
  final processor = DataProcessor();
  processor.process(3, 5);
}

// 输出:
// [调用方法] process(3, 5)
// [返回结果] process: 11

2. 缓存功能 Mixin

给网络请求类添加缓存能力:

import 'dart:async';

// 缓存Mixin,支持设置过期时间
mixin Cacheable {
  final Map<String, _CacheItem> _cache = {};

  // 缓存数据
  void cacheData(String key, dynamic data, {Duration? expiration}) {
    _cache[key] = _CacheItem(
      data: data,
      expiration: expiration != null ? DateTime.now().add(expiration) : null,
    );
  }

  // 获取缓存(如果过期则返回null)
  dynamic getCachedData(String key) {
    final item = _cache[key];
    if (item == null) return null;

    if (item.expiration != null && item.expiration!.isBefore(DateTime.now())) {
      _cache.remove(key); // 移除过期缓存
      return null;
    }

    return item.data;
  }
}

// 缓存项内部类
class _CacheItem {
  final dynamic data;
  final DateTime? expiration;

  _CacheItem({required this.data, this.expiration});
}

// 网络请求类
class ApiClient with Cacheable {
  // 模拟网络请求
  Future<String> fetchData(String url) async {
    // 先检查缓存
    final cached = getCachedData(url);
    if (cached != null) {
      print("从缓存获取: $url");
      return cached;
    }

    // 模拟网络请求延迟
    await Future.delayed(Duration(seconds: 1));
    final result = "来自 $url 的数据";

    // 缓存10秒
    cacheData(url, result, expiration: Duration(seconds: 10));
    print("网络请求并缓存: $url");

    return result;
  }
}

void main() async {
  final api = ApiClient();

  // 第一次请求(网络)
  print(await api.fetchData("https://api.example.com/user"));

  // 第二次请求(缓存)
  print(await api.fetchData("https://api.example.com/user"));
}

3. 多个 Mixin 组合使用

一个类可以同时混入多个功能,实现功能的灵活组合:

// 可打印
mixin Printable {
  void printInfo() => print(toString());
}

// 可克隆
mixin Cloneable<T> {
  T clone();
}

// 数据模型类
class User with Printable, Cloneable<User> {
  final String name;
  final int age;

  User({required this.name, required this.age});

  @override
  String toString() => "User(name: $name, age: $age)";

  @override
  User clone() {
    return User(name: name, age: age);
  }
}

void main() {
  final user = User(name: "Alice", age: 25);

  // 使用Printable功能
  user.printInfo(); // 输出:User(name: Alice, age: 25)

  // 使用Cloneable功能
  final user2 = user.clone();
  user2.printInfo(); // 输出:User(name: Alice, age: 25)
}


四、Mixin 与其他代码复用方式的对比

方式特点适用场景
继承单继承,强调 "是一个" 的关系类之间有明确的层次结构
Mixin多混入,强调 "具有某种功能"给不同类添加通用功能
接口只定义契约,不包含实现规定类必须实现的方法
组合将对象作为成员变量,"有一个" 的关系复杂功能的模块化组合

最佳实践:

  • 优先使用组合Mixin实现代码复用
  • 谨慎使用继承,避免过深的继承层次
  • 接口用于定义规范,确保不同类的行为一致性


五、Dart 中的常用系统 Mixin

Dart 标准库中包含一些常用的 Mixin,例如:

  1. ObjectWithHashCode:帮助实现哈希码
  2. IterableMixin:简化迭代器实现
  3. ListMixin:简化列表实现

示例:使用 ListMixin 快速实现自定义列表

import 'dart:collection';

// 自定义列表,只允许偶数
class EvenList with ListMixin<int> {
  final List<int> _list = [];

  @override
  int length = 0;

  @override
  int operator [](int index) => _list[index];

  @override
  void operator []=(int index, int value) {
    if (value % 2 != 0) {
      throw ArgumentError("只能添加偶数");
    }
    _list[index] = value;
  }

  @override
  void add(int value) {
    if (value % 2 != 0) {
      throw ArgumentError("只能添加偶数");
    }
    _list.add(value);
    length++;
  }
}

void main() {
  final evenList = EvenList();
  evenList.add(2);
  evenList.add(4);
  print(evenList); // 输出:[2, 4]

  // 尝试添加奇数会报错
  try {
    evenList.add(3);
  } catch (e) {
    print(e); // 输出:Invalid argument(s): 只能添加偶数
  }
}