Dart: Dart 枚举与扩展方法 — 增强枚举与无侵入式扩展
枚举让有限状态有名字,扩展让旧类型有新能力 — 都是不修改源码就能增强代码的利器。
1. 你将学到
- 增强枚举(Enhanced Enums):属性、构造、方法
- 枚举与 switch 的配合
- 扩展方法(Extension Methods):定义与使用
- 扩展与私有性、命名冲突解决
- Bob 场景:
OrderStatus枚举 + String 扩展(金额格式化为 USD)
2. 一个开发者的真实故事
(1) 痛点:用字符串模拟状态导致拼写错误
Alice 在代码中用字符串表示订单状态:'pending'、'shipped'、'delivered'。一次拼写错误 'shiped' 没有被编译器发现,导致订单永远卡在"未发货"状态,客户投诉 200 单。她还经常写 if (status == 'pending' || status == 'processing') 这种容易遗漏的判断。
(2) 枚举的解法
Dart 的增强枚举让每个状态有类型安全的名字、附加属性和方法。switch 表达式保证穷尽性,漏掉一个状态编译器就报错。
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',
};
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
(3) 收益
- 拼写错误从运行时提前到编译时,状态相关 bug 减少 90%
- 穷尽 switch 保证不会遗漏任何状态
- 扩展方法让 String 和 num 无需子类化就能添加业务方法
3. 增强枚举
(1) 基础枚举
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:简单枚举
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
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
(2) 增强枚举
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:带属性的增强枚举
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
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:带方法的增强枚举
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()}');
}
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
4. 枚举与 switch 配合
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:穷尽 switch
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
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 特性 | if-else | switch 语句 | switch 表达式 |
|---|---|---|---|
| 穷尽检查 | 无 | 无 | 有(枚举) |
| 编译时保证 | 无 | 无 | 有 |
- 添加新枚举值时 | 可能遗漏 | 可能遗漏 | 编译报错 |
5. 扩展方法
(1) 基础扩展
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:String 扩展
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
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:num 扩展(金额格式化)
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
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:List 扩展
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
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
6. 扩展与私有性、命名冲突
(1) 命名冲突解决
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:命名空间解决冲突
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
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 冲突场景 | 解决方式 |
|---|---|
| 两个扩展有同名方法 | 用 ExtensionName(obj).method() 显式调用 |
| 扩展方法与类方法同名 | 类方法优先,扩展方法被隐藏 |
| 两个扩展在不同文件 | import 的扩展优先级取决于导入顺序 |
7. Bob 场景:OrderStatus 枚举 + String 扩展
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:DataPipeline 枚举与扩展实战
// 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');
}
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
8. 完整示例:DataPipeline 订单状态机
// ============================================
// 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}');
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
输出:
=== 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 缓存。
📖 小节
- 增强枚举让枚举值拥有属性、构造函数和方法,比普通字符串常量更安全
- switch 表达式与枚举配合,编译器保证穷尽性,新增枚举值不会遗漏
- 扩展方法为已有类型添加功能,无需修改源码,不改变内存布局
- 扩展方法静态分派,只能访问公有成员,命名冲突需显式解决
- DataPipeline 用 OrderStatus 枚举定义状态机,String/num 扩展格式化金额
📝 作业
- 基础题(难度⭐):定义一个
OutputFormat增强枚举,包含 json/csv/html 三个值,每个值有 fileExtension 属性(如 '.json')和 mimeType 属性(如 'application/json')。 - 进阶题(难度⭐⭐):为
String添加扩展方法toOrderId(格式化为 ORD-XXX)、isValidEmail(验证邮箱格式)、truncateWithEllipsis(int max)(截断并加省略号),并测试。 - 挑战题(难度⭐⭐⭐):用增强枚举实现一个完整的工作流状态机(Draft → Review → Approved → Published),每个状态定义允许的转换目标,实现
transitionTo()方法并验证非法转换被拒绝。