Dart 异常处理
异常是程序运行时发生的错误事件。
良好的异常处理能让程序在遇到错误时优雅地降级,而不是直接崩溃。
本章介绍 Dart 中的 try/catch/finally、throw 抛出异常、on 捕获指定类型以及 rethrow 重新抛出。
try / catch / finally 基础
try 块中放置可能抛出异常的代码,catch 块捕获并处理异常,finally 块无论是否异常都会执行。
实例
// 基本的 try-catch
try {
int result = 10 ~/ 0; // 除零操作会抛出异常
print('结果: $result'); // 这行不会执行
} catch (e) {
// e 是异常对象
print('捕获到异常: $e');
}
// 获取异常和堆栈信息
try {
List<int> numbers = [1, 2, 3];
print(numbers[10]); // 访问越界索引
} catch (e, stackTrace) {
// e 是异常,stackTrace 是调用堆栈
print('RUNOOB 异常: $e');
print('堆栈信息:\n$stackTrace');
}
// finally:无论是否异常都会执行
try {
print('尝试执行操作...');
int result = 5 ~/ 0;
print('这行不会执行: $result');
} catch (e) {
print('出错: $e');
} finally {
// finally 块中的代码始终执行,常用于清理资源
print('清理资源完成(finally 总是执行)');
}
}
捕获到异常: IntegerDivisionByZeroException RUNOOB 异常: RangeError (index): Invalid value: Not in inclusive range 0..2: 10 堆栈信息: #0 List.[] (dart:core-patch/growable_array.dart:264) #1 main (file:///...) ... 尝试执行操作... 出错: IntegerDivisionByZeroException 清理资源完成(finally 总是执行)
finally 块最适合用于资源清理,比如关闭文件、释放数据库连接等。即使 try 或 catch 中有 return 语句,finally 仍然会执行。
throw 抛出异常
使用 throw 关键字可以主动抛出异常。
Dart 允许抛出任何对象作为异常(不仅仅是 Exception 的子类),但最佳实践是抛出 Exception 或 Error 的子类。
实例
class InvalidAgeException implements Exception {
final int age;
final String message;
InvalidAgeException(this.age)
: message = '无效的年龄: $age(年龄必须在 0 到 150 之间)';
@override
String toString() => 'InvalidAgeException: $message';
}
// 使用自定义异常的函数
void validateAge(int age) {
if (age < 0) {
throw InvalidAgeException(age);
}
if (age > 150) {
throw InvalidAgeException(age);
}
print('年龄 $age 验证通过');
}
// 抛出内置异常
int divide(int a, int b) {
if (b == 0) {
throw ArgumentError('除数不能为 0'); // 使用内置异常
}
return a ~/ b;
}
void main() {
// 测试自定义异常
try {
validateAge(-5);
} catch (e) {
print(e);
}
try {
validateAge(200);
} catch (e) {
print(e);
}
validateAge(25); // 正常情况
// 测试抛出内置异常
try {
divide(10, 0);
} catch (e) {
print('RUNOOB 错误: $e');
}
}
InvalidAgeException: 无效的年龄: -5(年龄必须在 0 到 150 之间) InvalidAgeException: 无效的年龄: 200(年龄必须在 0 到 150 之间) 年龄 25 验证通过 RUNOOB 错误: Invalid argument(s): 除数不能为 0
Exception 和 Error 的区别:Exception 是可预期的、程序可以处理的错误(如网络超时、文件不存在);Error 是不可预期的、通常表示程序 bug 的问题(如类型错误、空指针)。你的代码应该捕获 Exception 而非 Error。
on 捕获指定类型
on 关键字可以按异常类型进行捕获,让错误处理更精细。
实例
// 按类型捕获不同的异常
try {
// 模拟不同类型的异常
int result = performOperation('divide_by_zero');
print('结果: $result');
} on IntegerDivisionByZeroException {
// 只捕获除零异常
print('RUNOOB 错误: 不能除以零');
} on FormatException catch (e) {
// 捕获格式异常,同时获取异常信息
print('RUNOOB 格式错误: ${e.message}');
} on RangeError {
// 只捕获范围错误
print('RUNOOB 错误: 索引越界');
} catch (e) {
// 兜底:捕获所有其他异常
print('未知错误: $e');
} finally {
print('操作结束\n');
}
// 按顺序匹配的示例
print('--- 测试不同的操作 ---');
for (var op in ['divide_by_zero', 'parse_error', 'out_of_range', 'normal']) {
try {
performOperation(op);
} on IntegerDivisionByZeroException {
print('$op -> 除零异常');
} on FormatException {
print('$op -> 格式异常');
} on RangeError {
print('$op -> 范围异常');
} catch (e) {
print('$op -> 其他异常: $e');
}
}
}
int performOperation(String operation) {
switch (operation) {
case 'divide_by_zero':
return 10 ~/ 0; // 抛出 IntegerDivisionByZeroException
case 'parse_error':
int.parse('not_a_number'); // 抛出 FormatException
return 0;
case 'out_of_range':
var list = [1, 2, 3];
return list[100]; // 抛出 RangeError
case 'normal':
return 42;
default:
throw Exception('未知操作');
}
}
RUNOOB 错误: 不能除以零 操作结束 --- 测试不同的操作 --- divide_by_zero -> 除零异常 parse_error -> 格式异常 out_of_range -> 范围异常
on 和 catch 的排列顺序很重要:Dart 会按照从上到下的顺序匹配异常类型。更具体的异常类型应该放在前面,更通用的放在后面。如果把 catch (e) 放在最前面,后面的 on 就永远不会被执行。
rethrow 重新抛出
有时你需要在捕获异常后做一些处理(如记录日志),然后让异常继续向上传播。
rethrow 关键字用于重新抛出当前捕获的异常,保留原始堆栈信息。
实例
void databaseOperation() {
print(' 正在连接数据库...');
throw Exception('RUNOOB 数据库连接失败');
}
void serviceLayer() {
try {
print(' 服务层: 调用数据库操作');
databaseOperation();
} catch (e) {
// 先记录日志,然后重新抛出
print(' [日志] 数据库操作异常: $e');
rethrow; // 保留原始异常和堆栈,继续向上传播
}
}
void controllerLayer() {
try {
print('控制器层: 处理请求');
serviceLayer();
} catch (e, stackTrace) {
// 最外层捕获并处理
print('控制器层捕获到异常');
print('错误信息: $e');
print('给用户返回: "服务暂时不可用,请稍后重试"');
}
}
void main() {
controllerLayer();
print('\n--- rethrow vs throw e 的区别 ---');
// 演示 rethrow 和 throw e 的区别
try {
try {
throw FormatException('原始异常');
} catch (e) {
// throw e:会重置堆栈信息,丢失原始调用位置
print('使用 throw e(丢失原始堆栈)...');
throw e; // 堆栈从这里开始
}
} catch (e, stack) {
print('捕获: $e');
print('堆栈指向这里(不是原始位置):\n$stack');
}
}
控制器层: 处理请求 服务层: 调用数据库操作 正在连接数据库... [日志] 数据库操作异常: Exception: RUNOOB 数据库连接失败 控制器层捕获到异常 错误信息: Exception: RUNOOB 数据库连接失败 给用户返回: "服务暂时不可用,请稍后重试" --- rethrow vs throw e 的区别 --- 使用 throw e(丢失原始堆栈)... 捕获: FormatException: 原始异常 堆栈指向这里(不是原始位置): ...
rethrow 和 throw e 的区别很关键:rethrow 保留原始异常的全部堆栈信息,方便定位问题根源;throw e 会重置堆栈,从当前位置重新开始。在记录日志后需要继续传播异常时,请使用 rethrow。
异常处理的最佳实践
| 实践 | 说明 |
|---|---|
| 只捕获能处理的异常 | 不要为了"不报错"而盲目 catch 所有异常。如果不知道如何处理,让它向上传播 |
| 按类型捕获 | 使用 on 指定异常类型,避免用一个 catch 吞掉所有异常 |
| finally 清理资源 | 文件、网络连接等资源在 finally 中关闭 |
| 不要吞掉异常 | 捕获了异常但什么都不做(空 catch 块)是危险的,至少记录日志 |
| 使用 rethrow | 需要记录日志后继续传播异常时,用 rethrow 而非 throw e |
| 异常消息要具体 | throw Exception('数据库连接失败: 192.168.1.1:3306') 比 throw Exception('出错了') 更有用 |
