掌握 Dart 的 sealed class(二)

257 阅读5分钟

当了解sealed class的模式匹配能力的时候,我第一想到的是枚举。

在 Dart 中 enumsealed class 都可以用于建模有限类型集合,但用途和灵活性有显著区别。

🔶 1. 使用场景对比

特性 / 对比点enum(增强枚举)sealed class(密封类)
用于建模有限状态✅ 非常适合(内建支持 values 等)✅ 更灵活但需手动处理
可携带不同字段⚠️ 只能有一个构造函数,所有枚举项字段一样✅ 各子类可有完全不同的字段
模式匹配✅ Dart 3 支持 switch exhaustiveness✅ Dart 3 switch 也完全支持
扩展性❌ 不支持继承枚举,不能新增项✅ 可以新增子类
方法和 getter✅ 支持✅ 支持
类型安全 & 约束✅ 限定值集合清晰✅ 更强表达能力
简洁性✅ 非常简洁❌ 需要更多代码
推荐用法✅ 状态、颜色、固定类型等✅ 多状态对象、响应类型、操作结果等

🔹 2. 示例对比:状态建模

✅ 使用 enum(适合状态固定,无需复杂数据)

enum ConnectionState {
  connected,
  disconnected,
  connecting,
}

✅ 使用 sealed class(更复杂状态,携带数据)

sealed class ConnectionState {}

class Connected extends ConnectionState {
  final DateTime timestamp;
  Connected(this.timestamp);
}

class Disconnected extends ConnectionState {}

class Connecting extends ConnectionState {
  final int retryCount;
  Connecting(this.retryCount);
}

🔸 模式匹配(Dart 3)

void handle(ConnectionState state) {
  switch (state) {
    case Connected():
      print('已连接');
      break;
    case Connecting(:var retryCount):
      print('重试次数: $retryCount');
      break;
    case Disconnected():
      print('已断开');
      break;
  }
}

🔹 3. 密封类的关联值优势

Dart 的增强枚举(Enhanced Enums,Dart 2.17+)允许为每个枚举值定义静态且统一的属性,但这些属性在编译时固定,无法动态扩展或差异化。例如:

enum DeviceType {
  lamp(value: 1, icon: 'lamp_icon'),
  airConditioner(value: 2, icon: 'ac_icon');

  final int value;
  final String icon;

  const DeviceType({required this.value, required this.icon});
}

特点​:

  • ​静态属性​:所有枚举成员必须定义相同的属性(如 value 和 icon),且类型一致。
  • ​不可扩展​:无法为不同枚举成员添加不同的方法或动态属性。
  • ​通过扩展函数补充逻辑​:需借助扩展函数实现额外行为(如转换字符串)

密封类的子类可以自由定义属性和方法,每个子类可独立携带不同数据,且支持动态扩展

sealed class Result {}
class Success implements Result {
  final String data;
  Success(this.data);
}
class Error implements Result {
  final int code;
  final String message;
  Error(this.code, this.message);
}

特点​:

​- 动态属性​:每个子类可定义独立的属性(如 Success 只有 data,Error 有 code 和 message)。 ​- 方法扩展​:子类可包含独立的方法逻辑。 ​- 编译时类型安全​:switch 表达式强制覆盖所有子类分支 ​

具体场景对比

场景 1:表示 API 错误

枚举实现(静态属性):

enum ApiErrorCode {
  network(code: 1001, message: '网络错误'),
  server(code: 5001, message: '服务器错误');

  final int code;
  final String message;

  const ApiErrorCode({required this.code, required this.message});
}

缺点:所有错误类型必须遵循相同的属性结构,无法为特定错误添加额外字段(如 details)。

密封类实现(动态属性):

sealed class ApiError {}
class NetworkError implements ApiError {
  final int code;
  final String message;
  final String details; // 额外字段
  NetworkError(this.code, this.message, this.details);
}
class ServerError implements ApiError {
  final int code;
  ServerError(this.code);
}

优势NetworkError 可携带 details,而 ServerError 无需冗余字段。

场景 2:状态管理

枚举实现(静态属性):

enum LoadingState {
  dataLoaded(data: '默认数据'),
  error(message: '默认错误');

  final String data;
  final String message;

  const LoadingState({required this.data, required this.message});
}

缺点:所有状态必须包含 datamessage,即使某些状态不需要(如纯加载中状态)。

密封类实现(动态属性):

sealed class LoadingState {}
class DataLoaded implements LoadingState {
  final String data;
  DataLoaded(this.data);
}
class Loading implements LoadingState {}
class ErrorState implements LoadingState {
  final String message;
  ErrorState(this.message);
}

优势:状态可按需携带数据,避免冗余。

总结

  1. 枚举关联值的局限性

    • 属性必须统一,无法差异化定义。
    • 无法为特定枚举值添加独立方法或逻辑。
    • 依赖扩展函数补充功能,代码分散。
  2. 密封类的优势

    • 子类可自由定义属性和方法,支持动态扩展。
    • 编译时强制类型检查,避免分支遗漏。
    • 代码结构更清晰,适合复杂业务场景。

🔶 4. 本质差异

  1. 设计目标

    • 枚举:用于表示固定且无状态的常量集合(如星期、方向)。
    • 密封类:用于表示有限但可扩展的类型集合,且每个类型可独立定义行为和状态。
  2. 扩展性

    • 枚举无法继承或扩展,新增类型需修改原枚举定义。
    • 密封类允许通过继承添加新类型,且编译器自动识别新类型(需在同一库)。
  3. 代码可读性

    • 密封类通过子类名称直接表达意图(如 NetworkErrorErrorType.network 更清晰)。
    • 枚举依赖附加值或额外方法解释状态细节。

✅ 5. 何时选择密封类而非枚举?

  1. 需要状态或行为差异:当每个类型需要携带不同数据或实现不同逻辑时。
  2. 严格的类型检查:依赖编译时强制覆盖所有分支的场景(如状态机)。
  3. 业务逻辑复杂度:涉及多层级状态或错误类型时,密封类能保持代码简洁和可维护性。

密封类通过类型安全的模式匹配灵活的状态扩展编译时检查,解决了枚举在复杂场景下的局限性。在 Flutter 开发中,密封类尤其适合用于状态管理、错误处理等需要明确类型分支的场景,而枚举更适合简单的常量集合。两者的选择需根据具体业务需求权衡。