Dart: Dart 枚举与扩展方法 — 增强枚举与无侵入式扩展

枚举让有限状态有名字,扩展让旧类型有新能力 — 都是不修改源码就能增强代码的利器。

1. 你将学到


2. 一个开发者的真实故事

(1) 痛点:用字符串模拟状态导致拼写错误

Alice 在代码中用字符串表示订单状态:'pending''shipped''delivered'。一次拼写错误 'shiped' 没有被编译器发现,导致订单永远卡在"未发货"状态,客户投诉 200 单。她还经常写 if (status == 'pending' || status == 'processing') 这种容易遗漏的判断。

(2) 枚举的解法

Dart 的增强枚举让每个状态有类型安全的名字、附加属性和方法。switch 表达式保证穷尽性,漏掉一个状态编译器就报错。

DART
enum OrderStatus {
  pending(label: 'Awaiting Processing', isFinal: false),
  shipped(label: 'In Transit', isFinal: false),
  delivered(label: 'Completed', isFinal: true),
  cancelled(label: 'Cancelled', isFinal: true);

  final String label;
  final bool isFinal;

  const OrderStatus({required this.label, required this.isFinal});
}

// Exhaustive switch - compiler checks all cases
String handle(OrderStatus status) => switch (status) {
  OrderStatus.pending => 'Queue for processing',
  OrderStatus.shipped => 'Track shipment',
  OrderStatus.delivered => 'Send survey',
  OrderStatus.cancelled => 'Process refund',
};
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

(3) 收益


3. 增强枚举

(1) 基础枚举

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:简单枚举

DART
enum OutputFormat {
  json,
  csv,
  html,
}

void main() {
  final format = OutputFormat.json;

  // Enum values
  print(format.name);          // json
  print(format.index);         // 0
  print(OutputFormat.values);  // [OutputFormat.json, OutputFormat.csv, OutputFormat.html]

  // Parse from string
  final parsed = OutputFormat.values.byName('csv');
  print(parsed);  // OutputFormat.csv
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

(2) 增强枚举

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:带属性的增强枚举

DART
enum OrderStatus {
  pending(label: 'Awaiting Processing', isFinal: false, priority: 1),
  processing(label: 'Being Processed', isFinal: false, priority: 2),
  shipped(label: 'In Transit', isFinal: false, priority: 3),
  delivered(label: 'Completed', isFinal: true, priority: 0),
  cancelled(label: 'Cancelled', isFinal: true, priority: 0);

  final String label;
  final bool isFinal;
  final int priority;

  const OrderStatus({required this.label, required this.isFinal, required this.priority});

  bool get isActive => !isFinal;

  String get displayName => '${name.toUpperCase()} - $label';
}

void main() {
  final status = OrderStatus.shipped;

  print(status.label);       // In Transit
  print(status.isFinal);     // false
  print(status.isActive);    // true
  print(status.displayName); // SHIPPED - In Transit
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:带方法的增强枚举

DART
enum TaxCategory {
  standard(rate: 0.08, label: 'Standard Rate'),
  reduced(rate: 0.05, label: 'Reduced Rate'),
  zero(rate: 0.0, label: 'Zero Rate'),
  exempt(rate: 0.0, label: 'Tax Exempt');

  final double rate;
  final String label;

  const TaxCategory({required this.rate, required this.label});

  double calculate(double amount) => amount * rate;

  double applyTo(double amount) => amount * (1 + rate);

  String formatRate() => '${(rate * 100).toStringAsFixed(1)}%';
}

void main() {
  final tax = TaxCategory.standard;
  print(tax.calculate(1500.0));   // 120.0
  print(tax.applyTo(1500.0));     // 1620.0
  print(tax.formatRate());        // 8.0%

  // All categories
  for (final cat in TaxCategory.values) {
    print('${cat.label}: ${cat.formatRate()}');
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

4. 枚举与 switch 配合

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:穷尽 switch

DART
enum DataSourceType {
  api,
  file,
  database,
}

String describeSource(DataSourceType type) => switch (type) {
  DataSourceType.api => 'REST API endpoint',
  DataSourceType.file => 'Local file system',
  DataSourceType.database => 'SQL database connection',
};

// With exhaustive check - compiler forces all cases
bool canRetry(DataSourceType type) => switch (type) {
  DataSourceType.api => true,      // API can retry
  DataSourceType.file => false,    // File errors need manual fix
  DataSourceType.database => true, // DB can retry with backoff
};

void main() {
  print(describeSource(DataSourceType.api));  // REST API endpoint
  print(canRetry(DataSourceType.file));       // false
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
特性 if-else switch 语句 switch 表达式
穷尽检查 有(枚举)
编译时保证

5. 扩展方法

(1) 基础扩展

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:String 扩展

DART
extension StringCurrency on String {
  String toUSD() => '\$$this USD';
  String toEUR() => '€${this} EUR';

  String truncate(int maxLength) =>
      length <= maxLength ? this : '${substring(0, maxLength)}...';

  String get capitalized =>
      isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}

void main() {
  print('1500.00'.toUSD());         // $1500.00 USD
  print('1200.00'.toEUR());        // €1200.00 EUR
  print('Very long product name'.truncate(10));  // Very long...
  print('electronics'.capitalized); // Electronics
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:num 扩展(金额格式化)

DART
extension NumFormatting on num {
  String toUSD() => '\$${toStringAsFixed(2)} USD';
  String toCompact() {
    if (this >= 1000000) return '\$${(this / 1000000).toStringAsFixed(1)}M USD';
    if (this >= 1000) return '\$${(this / 1000).toStringAsFixed(1)}K USD';
    return toUSD();
  }

  double get asK => this / 1000;
  double get asM => this / 1000000;

  bool isBetween(num from, num to) => from <= this && this <= to;
}

void main() {
  print(1500.0.toUSD());       // $1500.00 USD
  print(1500000.0.toCompact()); // $1.5M USD
  print(5000.asK);             // 5.0
  print(1500.0.isBetween(1000, 2000)); // true
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:List 扩展

DART
extension ListStats on List``<double>`` {
  double get sum => fold(0, (a, b) => a + b);
  double get average => isEmpty ? 0 : sum / length;
  double get median {
    final sorted = [...this]..sort();
    final mid = length ~/ 2;
    return length.isEven
        ? (sorted[mid - 1] + sorted[mid]) / 2
        : sorted[mid];
  }
}

void main() {
  final amounts = [1500.0, 3200.0, 890.0, 50.0];
  print('Sum: ${amounts.sum.toUSD()}');       // Sum: $5640.00 USD
  print('Average: ${amounts.average.toUSD()}'); // Average: $1410.00 USD
  print('Median: ${amounts.median.toUSD()}');  // Median: $1195.00 USD
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

6. 扩展与私有性、命名冲突

(1) 命名冲突解决

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:命名空间解决冲突

DART
extension MathExtras on num {
  int get squared => (this * this).toInt();
}

extension StringExtras on String {
  String get reversed => split('').reversed.join('');
}

// If two extensions have the same method name
extension DoubleExtras on double {
  String toMoney() => '\$${toStringAsFixed(2)}';
}

extension IntExtras on int {
  String toMoney() => '\$${this}.00';
}

void main() {
  // Direct call - compiler resolves by type
  print(5.squared);            // 25
  print('hello'.reversed);     // olleh

  // Explicit resolution when ambiguous
  print(DoubleExtras(1500.5).toMoney());  // $1500.50
  print(IntExtras(1500).toMoney());       // $1500.00
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
冲突场景 解决方式
两个扩展有同名方法 ExtensionName(obj).method() 显式调用
扩展方法与类方法同名 类方法优先,扩展方法被隐藏
两个扩展在不同文件 import 的扩展优先级取决于导入顺序

7. Bob 场景:OrderStatus 枚举 + String 扩展

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:DataPipeline 枚举与扩展实战

DART
// Order status enum with business logic
enum OrderStatus {
  pending(label: 'Awaiting Processing', isFinal: false),
  processing(label: 'Being Processed', isFinal: false),
  shipped(label: 'In Transit', isFinal: false),
  delivered(label: 'Completed', isFinal: true),
  cancelled(label: 'Cancelled', isFinal: true),
  refunded(label: 'Refunded', isFinal: true);

  final String label;
  final bool isFinal;

  const OrderStatus({required this.label, required this.isFinal});

  bool get isActive => !isFinal;
  bool get canCancel => this == pending || this == processing;
  bool get canRefund => this == delivered;
}

// String extension for DataPipeline formatting
extension DataPipelineString on String {
  String get asOrderId => 'ORD-$this';
  String toUSD() => '\$$this USD';
  String toCategoryLabel => split('_').map((w) => w.capitalizeFirst).join(' ');
}

extension StringCap on String {
  String get capitalizeFirst =>
      isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}

// num extension for revenue formatting
extension RevenueFormatting on num {
  String toRevenue() => '\$${toStringAsFixed(2)} USD';
  String toCompactRevenue() {
    if (this >= 1000000) return '\$${(this / 1000000).toStringAsFixed(1)}M USD';
    if (this >= 1000) return '\$${(this / 1000).toStringAsFixed(1)}K USD';
    return toRevenue();
  }
}

void main() {
  // Enum usage
  final status = OrderStatus.shipped;
  print('Status: ${status.label}');      // In Transit
  print('Active: ${status.isActive}');   // true
  print('Can cancel: ${status.canCancel}'); // false

  // String extensions
  print('001'.asOrderId);               // ORD-001
  print('1500.00'.toUSD());             // $1500.00 USD

  // Revenue formatting
  print(1500000.toCompactRevenue());     // $1.5M USD
  print(52500.75.toRevenue());           // $52500.75 USD

  // Exhaustive switch on enum
  for (final s in OrderStatus.values) {
    final action = switch (s) {
      OrderStatus.pending => 'Queue for processing',
      OrderStatus.processing => 'Monitor progress',
      OrderStatus.shipped => 'Track delivery',
      OrderStatus.delivered => 'Send confirmation',
      OrderStatus.cancelled => 'Process cancellation',
      OrderStatus.refunded => 'Update records',
    };
    print('  ${s.name}: $action');
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

8. 完整示例:DataPipeline 订单状态机

DART
// ============================================
// DataPipeline Order State Machine
// Enhanced enums + extensions in action
// ============================================

enum OrderStatus {
  pending(label: 'Awaiting Processing', isFinal: false, color: 'yellow'),
  processing(label: 'Being Processed', isFinal: false, color: 'blue'),
  shipped(label: 'In Transit', isFinal: false, color: 'orange'),
  delivered(label: 'Completed', isFinal: true, color: 'green'),
  cancelled(label: 'Cancelled', isFinal: true, color: 'red'),
  refunded(label: 'Refunded', isFinal: true, color: 'gray');

  final String label;
  final bool isFinal;
  final String color;

  const OrderStatus({
    required this.label,
    required this.isFinal,
    required this.color,
  });

  bool get isActive => !isFinal;
  bool get canTransition => !isFinal;

  List``<OrderStatus>`` get allowedTransitions => switch (this) {
    pending => [processing, cancelled],
    processing => [shipped, cancelled],
    shipped => [delivered],
    delivered => [refunded],
    cancelled => [],
    refunded => [],
  };

  bool canTransitionTo(OrderStatus target) =>
      allowedTransitions.contains(target);
}

extension NumRevenue on num {
  String toUSD() => '\$${toStringAsFixed(2)} USD';
}

class Order {
  final String id;
  final double amount;
  OrderStatus status;

  Order({required this.id, required this.amount, this.status = OrderStatus.pending});

  bool transitionTo(OrderStatus newStatus) {
    if (!status.canTransitionTo(newStatus)) {
      print('  Cannot transition from ${status.name} to ${newStatus.name}');
      return false;
    }
    print('  $id: ${status.name} → ${newStatus.name}');
    status = newStatus;
    return true;
  }

  String get summary => '$id: ${status.label} (${amount.toUSD()})';
}

void main() {
  final order = Order(id: 'ORD-001', amount: 1500.0);

  print('=== Order State Machine ===');
  print('Initial: ${order.summary}');

  // Valid transitions
  order.transitionTo(OrderStatus.processing);  // OK
  order.transitionTo(OrderStatus.shipped);      // OK
  order.transitionTo(OrderStatus.delivered);    // OK

  // Invalid transition
  order.transitionTo(OrderStatus.cancelled);  // Cannot: delivered → cancelled

  // Valid refund
  order.transitionTo(OrderStatus.refunded);   // OK

  print('\nFinal: ${order.summary}');

  // Print all states and transitions
  print('\n=== State Transition Table ===');
  for (final status in OrderStatus.values) {
    final targets = status.allowedTransitions.map((t) => t.name).join(', ');
    print('  ${status.name.padRight(12)} → ${targets.isEmpty ? '(final)' : targets}');
  }

  // Status statistics
  print('\n=== Status Properties ===');
  final activeCount = OrderStatus.values.where((s) => s.isActive).length;
  final finalCount = OrderStatus.values.where((s) => s.isFinal).length;
  print('Active states: $activeCount');
  print('Final states:  $finalCount');
  print('Total states:  ${OrderStatus.values.length}');
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

输出:

TEXT 📖 仅展示
=== Order State Machine ===
Initial: ORD-001: Awaiting Processing ($1500.00 USD)
  ORD-001: pending → processing
  ORD-001: processing → shipped
  ORD-001: shipped → delivered
  Cannot transition from delivered to cancelled
  ORD-001: delivered → refunded

Final: ORD-001: Refunded ($1500.00 USD)

=== State Transition Table ===
  pending      → processing, cancelled
  processing   → shipped, cancelled
  shipped      → delivered
  delivered    → refunded
  cancelled    → (final)
  refunded     → (final)

=== Status Properties ===
Active states: 3
Final states:  3
Total states:  6

❓ 常见问题

Q:增强枚举和普通枚举有什么区别? A:增强枚举可以有属性、构造函数和方法,普通枚举只有 name 和 index。Dart 2.17+ 推荐全部用增强枚举。

Q:枚举可以实现接口吗? A:可以。枚举可以实现接口,如 `enum Status implements Comparable`````。但不能 extends 其他类(枚举隐式继承 Enum)。

Q:扩展方法能访问私有成员吗? A:不能。扩展方法在类外部定义,只能访问公有成员。这是扩展和类方法的根本区别。

Q:扩展方法是静态分派还是动态分派? A:静态分派。编译时根据变量的声明类型决定调用哪个扩展方法,运行时类型不影响。这与类方法是动态分派的本质区别。

Q:扩展可以添加属性吗? A:可以添加计算属性(getter),但不能添加实例变量(存储属性)。扩展不修改对象的内存布局。

Q:两个扩展定义了同名方法会怎样? A:如果调用时能根据接收者类型区分,编译器自动选择。如果不能区分(歧义),需要用 ExtensionName(obj).method() 显式指定。

Q:枚举的 values 和 byName 有性能差异吗? A:values 返回缓存的列表,O(1);byName 遍历 values 查找,O(n)。频繁查找时建议自己创建 Map 缓存。


📖 小节


📝 作业

  1. 基础题(难度⭐):定义一个 OutputFormat 增强枚举,包含 json/csv/html 三个值,每个值有 fileExtension 属性(如 '.json')和 mimeType 属性(如 'application/json')。
  2. 进阶题(难度⭐⭐):为 String 添加扩展方法 toOrderId(格式化为 ORD-XXX)、isValidEmail(验证邮箱格式)、truncateWithEllipsis(int max)(截断并加省略号),并测试。
  3. 挑战题(难度⭐⭐⭐):用增强枚举实现一个完整的工作流状态机(Draft → Review → Approved → Published),每个状态定义允许的转换目标,实现 transitionTo() 方法并验证非法转换被拒绝。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏