异常
Exception
异常的基本概念与抛出(throw)
理论
在Dart中,异常(Exception)是指程序在运行时发生的非预期的错误情况,它会中断程序的正常执行流程。当一个异常发生时,如果它没有被捕获处理,程序就会终止并报告错误。
Dart中的异常处理机制允许我们优雅地处理这些错误,而不是让程序崩溃。
要手动触发或 抛出(throw)一个异常,我们可以使用throw关键字,后跟任意对象。通常,我们会抛出Exception类或其子类的实例,或者Error类或其子类的实例。
注意: 实践中,我们通常抛出那些扩展自Exception或Error的对象,以提供更结构化的错误信息。
示例
// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM
void checkAge(int age) {
if (age < 0) {
// 抛出一个 ArgumentError 异常。ArgumentError 是 Error 的一个子类。
throw ArgumentError('年龄不能为负数。Received age: $age'); // 英文原文:Argument Error
} else if (age < 18) {
// 抛出一个FormatException异常。FormatException 是 Exception 的一个子类。
throw FormatException('年龄太小不允许访问。Received age: $age'); // 英文原文:Format Exception
} else {
print('年龄:$age,访问已授权。');
}
}
void main() {
print('--- 尝试有效年龄 ---');
checkAge(20);
print('\n--- 尝试负数年龄 (将抛出异常) ---');
try {
checkAge(-5); // 这里的异常会中断后续代码执行,但我们可以try-catch捕获
} catch (e) {
print('捕获到异常(主函数中):${e.runtimeType} - $e');
}
print('\n--- 尝试过小年龄 (将抛出异常) ---');
try {
checkAge(10); // 这里的异常会中断后续代码执行,但我们可以try-catch捕获
} catch (e) {
print('捕获到异常(主函数中):${e.runtimeType} - $e');
}
print('\n程序执行完毕。');
}
运行结果
--- 尝试有效年龄 ---
年龄:20,访问已授权。
--- 尝试负数年龄 (将抛出异常) ---
捕获到异常(主函数中):ArgumentError - Invalid argument(s): 年龄不能为负数。Received age: -5
--- 尝试过小年龄 (将抛出异常) ---
捕获到异常(主函数中):FormatException - FormatException: 年龄太小不允许访问。Received age: 10
程序执行完毕。
注意事项
- 未捕获的异常会导致程序终止: 如果一个异常被抛出,但没有被任何
try-catch块捕获,程序将立即停止执行。 throw可以抛出任何对象: 虽然可以抛出任何对象,但强烈建议抛出Exception或Error的子类,这有助于接收方更好地理解和处理错误类型。- 异常是运行时错误: 异常与编译时错误不同,编译时错误会在代码编译阶段被发现,而异常在程序运行时才可能发生。
捕获与处理异常(try-on-catch)
理论
为了防止异常导致程序崩溃,我们需要捕获(catch)它们。Dart提供了try、on和catch关键字来处理异常。
try块: 包含可能抛出异常的代码。on关键字: 用于指定要捕获的特定异常类型。如果try块中发生了指定类型的异常,on块将被执行。catch关键字: 用于捕获任何类型的异常,或者与on结合使用以获取异常对象本身以及堆栈跟踪。
语法结构
try {
// 可能会抛出异常的代码
} on SpecificExceptionType {
// 处理特定类型的异常
} on AnotherSpecificExceptionType catch (e) {
// 处理另一种特定类型的异常,并访问异常对象 e
} catch (e) {
// 捕获所有其他类型的异常,并访问异常对象 e
} catch (e, s) {
// 捕获所有其他类型的异常,并访问异常对象 e 和堆栈跟踪 s
}
注意: on块和catch块可以单独使用,也可以组合使用。
on只能捕获特定类型的异常。catch可以捕获任何异常,并提供异常对象。on SpecificExceptionType catch (e)结合了两者的功能,捕获特定类型异常并获取异常对象。- 异常捕获的顺序很重要,更具体的异常类型应该放在前面,更通用的异常类型放在后面。
示例
// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM
void riskyOperation(int value) {
if (value < 0) {
throw ArgumentError('值不能为负数。');
} else if (value == 0) {
throw StateError('值不能为零,因为它会导致一个无效状态。'); // 英文原文:State Error
} else if (value > 100) {
throw Exception('值过大,超出处理范围。'); // 英文原文:Exception
}
print('操作成功,值为: $value');
}
void main() {
print('--- 尝试有效操作 ---');
try {
riskyOperation(50);
} on ArgumentError catch (e) {
print('捕获到 ArgumentError: $e'); // 这不会被触发
} catch (e, stackTrace) {
print('捕获到通用异常: ${e.runtimeType} - $e');
print('堆栈跟踪: $stackTrace'); // 这不会被触发
}
print('\n--- 尝试负数 (捕获 ArgumentError) ---');
try {
riskyOperation(-1);
} on ArgumentError catch (e) { // 捕获特定类型的ArgumentError
print('捕获到 ArgumentError: $e');
} on StateError catch (e) {
print('捕获到 StateError: $e');
} catch (e, s) { // 捕获其他所有异常
print('捕获到通用异常: ${e.runtimeType} - $e');
print('堆栈跟踪: $s');
}
print('\n--- 尝试零 (捕获 StateError) ---');
try {
riskyOperation(0);
} on ArgumentError catch (e) {
print('捕获到 ArgumentError: $e');
} on StateError catch (e) { // 捕获特定类型的StateError
print('捕获到 StateError: $e');
} catch (e, s) {
print('捕获到通用异常: ${e.runtimeType} - $e');
print('堆栈跟踪: $s');
}
print('\n--- 尝试过大值 (捕获通用 Exception) ---');
try {
riskyOperation(200);
} on ArgumentError catch (e) {
print('捕获到 ArgumentError: $e');
} on StateError { // 可以只用on,不捕获异常对象
print('捕获到 StateError (没有异常对象)');
} catch (e, s) { // 捕获所有其他异常,包括 Exception
print('捕获到通用异常: ${e.runtimeType} - $e');
print('堆栈跟踪: $s');
}
print('\n程序执行完毕。');
}
运行结果
--- 尝试有效操作 ---
操作成功,值为: 50
--- 尝试负数 (捕获 ArgumentError) ---
捕获到 ArgumentError: Invalid argument(s): 值不能为负数。
--- 尝试零 (捕获 StateError) ---
捕获到 StateError: Bad state: 值不能为零,因为它会导致一个无效状态。
--- 尝试过大值 (捕获通用 Exception) ---
捕获到通用异常: Exception - Exception: 值过大,超出处理范围。
堆栈跟踪: #0 riskyOperation (file:///.../main.dart:20:5)
#1 main (file:///.../main.dart:67:5)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
程序执行完毕。
注意事项
- 异常捕获的顺序: 总是将最具体的异常类型放在
on块的前面,将更通用的异常类型放在后面。如果catch (e)放在最前面,它会捕获所有异常,导致后面的on SpecificExceptionType永不被执行。 stackTrace的作用:stackTrace对象提供了异常发生时的方法调用序列,这对于调试非常有用,尤其是在生产环境中记录错误时。- 资源清理: 即使发生异常,有时也需要确保某些资源(如文件句柄、网络连接)被关闭。这正是
finally块的用武之地。
finally块
理论
finally块包含的代码无论try块中是否发生异常,也无论异常是否被捕获,都会在try和catch块执行完毕后执行。它通常用于执行清理操作,例如关闭文件、释放资源或取消网络请求。
语法结构
try {
// 可能会抛出异常的代码
} on SpecificExceptionType {
// 处理特定类型的异常
} catch (e) {
// 捕获所有其他类型的异常
} finally {
// 无论是否发生异常,这部分代码总会执行
}
注意: 即使try块中通过return语句提前退出,或者抛出一个未被捕获的异常,finally块也依然会执行。
示例
// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM
void processFile(String fileName, String content) {
// 模拟文件操作
print('开始处理文件: $fileName');
bool fileOpened = false; // 模拟资源(文件)是否被打开
try {
print('尝试打开文件...');
fileOpened = true; // 模拟文件已打开
if (fileName.isEmpty) {
throw FormatException('文件名不能为空。');
}
if (content.length > 20) {
throw Exception('文件内容过长,当前限制20字符。');
}
print('文件内容写入成功: "$content"');
} on FormatException catch (e) {
print('捕获到文件名格式错误: $e');
} catch (e) {
print('捕获到通用文件处理异常: ${e.runtimeType} - $e');
} finally {
// 无论是否发生异常,文件都应该关闭
if (fileOpened) {
print('文件已关闭。');
} else {
print('文件未被打开,无需关闭。');
}
print('文件处理流程结束。');
}
}
void main() {
print('--- 正常文件处理 ---');
processFile('report.txt', '这是报告内容');
print('\n--- 文件名为空 (FormatException) ---');
processFile('', '一些内容');
print('\n--- 内容过长 (Exception) ---');
processFile('long_data.txt', '这是一个非常非常长的内容,超过了20个字符限制');
print('\n程序执行完毕。');
}
运行结果
--- 正常文件处理 ---
开始处理文件: report.txt
尝试打开文件...
文件内容写入成功: "这是报告内容"
文件已关闭。
文件处理流程结束。
--- 文件名为空 (FormatException) ---
开始处理文件:
尝试打开文件...
捕获到文件名格式错误: FormatException: 文件名不能为空。
文件已关闭。
文件处理流程结束。
--- 内容过长 (Exception) ---
开始处理文件: long_data.txt
尝试打开文件...
捕获到通用文件处理异常: Exception - Exception: 文件内容过长,当前限制20字符。
文件已关闭。
文件处理流程结束。
程序执行完毕。
注意事项
- 确保资源释放:
finally块是执行清理逻辑的最佳位置,例如关闭数据库连接、网络套接字或文件流。 finally不改变异常流程:finally块的执行不会改变try块中异常的传播行为。如果异常未被catch捕获,它会在finally块执行完毕后继续向上层传播。- 在
finally中抛出异常: 如果在finally块中再次抛出异常,这个新异常会覆盖try块(或catch块)中可能抛出的任何未处理的异常。通常应避免在finally块中抛出异常。
自定义异常
理论
Dart允许我们创建自己的自定义异常类型。这在需要表示应用程序特有的错误条件时非常有用,它能提高代码的可读性、可维护性,并允许我们进行更精细的错误处理。
自定义异常通常是通过继承Exception类(或者更具体的如FormatException、StateError等)来实现的。继承Error类也是一种选择,但通常Error表示程序更严重的、不应该被捕获的编程错误(稍后解释)。
创建自定义异常的步骤:
- 创建一个新的类。
- 让这个类继承
Exception或其它合适的内置异常类。 - 根据需要添加构造函数和字段,以提供详细的错误信息。
- 重写
toString()方法以提供友好的异常描述。
示例
// domain_exceptions.dart
// 这是自定义异常的定义文件
// Dart SDK 版本: 2.19.0 或更高
/// 表示用户认证失败的自定义异常。
class AuthenticationException implements Exception {
final String message;
final String? errorCode; // 可选的错误码
AuthenticationException(this.message, {this.errorCode});
String toString() {
return 'AuthenticationException: $message' +
(errorCode != null ? ' (Code: $errorCode)' : '');
}
}
/// 表示数据格式错误的自定义异常。
class InvalidDataException implements Exception {
final String fieldName;
final String invalidValue;
final String? expectedFormat;
InvalidDataException(this.fieldName, this.invalidValue, {this.expectedFormat});
String toString() {
return 'InvalidDataException: 字段 "$fieldName" 的值 "$invalidValue" 无效。' +
(expectedFormat != null ? ' 期望格式: $expectedFormat' : '');
}
}
// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM
import 'domain_exceptions.dart'; // 导入自定义异常
void performLogin(String username, String password) {
if (username.isEmpty || password.isEmpty) {
throw InvalidDataException('username/password', 'empty', expectedFormat: '非空字符串');
}
if (username != 'admin' || password != 'password123') {
throw AuthenticationException('用户名或密码不正确', errorCode: 'AUTH_001');
}
print('用户 "$username" 登录成功!');
}
void main() {
print('--- 尝试有效登录 ---');
try {
performLogin('admin', 'password123');
} catch (e) {
print('捕获到异常: $e'); // 这不会被触发
}
print('\n--- 尝试空用户名 (InvalidDataException) ---');
try {
performLogin('', 'password');
} on InvalidDataException catch (e) {
print('捕获到自定义异常 (InvalidDataException): $e');
} on AuthenticationException catch (e) {
print('捕获到自定义异常 (AuthenticationException): $e');
} catch (e, s) {
print('捕获到通用异常: ${e.runtimeType} - $e');
print('堆栈跟踪: $s');
}
print('\n--- 尝试错误密码 (AuthenticationException) ---');
try {
performLogin('admin', 'wrong_password');
} on InvalidDataException catch (e) {
print('捕获到自定义异常 (InvalidDataException): $e');
} on AuthenticationException catch (e) {
print('捕获到自定义异常 (AuthenticationException): $e');
} catch (e, s) {
print('捕获到通用异常: ${e.runtimeType} - $e');
print('堆栈跟踪: $s');
}
print('\n程序执行完毕。');
}
运行结果
--- 尝试有效登录 ---
用户 "admin" 登录成功!
--- 尝试空用户名 (InvalidDataException) ---
捕获到自定义异常 (InvalidDataException): InvalidDataException: 字段 "username/password" 的值 "empty" 无效。 期望格式: 非空字符串
--- 尝试错误密码 (AuthenticationException) ---
捕获到自定义异常 (AuthenticationException): AuthenticationException: 用户名或密码不正确 (Code: AUTH_001)
程序执行完毕。
注意事项
- 为业务逻辑创建: 自定义异常应该用于表示应用程序的业务逻辑错误,而不是程序内部的缺陷。
- 提供足够的信息: 在自定义异常中包含足够的信息(如错误消息、错误码、导致错误的数据等),以便在捕获时能够进行有效的诊断和处理。
- 继承
Exception: 大多数情况下,自定义异常应该继承Exception而不是Error。Error通常用于表示程序无法恢复的编程错误。 - 实现
toString(): 重写toString()方法使得异常在打印时能够提供有意义的信息。
异步操作中的异常处理
理论
在Dart中,异步编程是核心特性,主要通过 Future 和 Stream 来实现。异步操作中的异常处理方式与同步操作有所不同。
-
对于
Future: 当一个Future失败时(即发生异常时),它会以一个错误(error)而非一个值(value)来完成。- 你可以使用
try-catch块来捕获await表达式之后抛出的异常。 - 或者,对于不使用
await的Future,可以使用Future.catchError方法来处理异常。
- 你可以使用
-
对于
Stream:Stream是一系列异步事件。当Stream处理过程中发生异常时,它会触发onError事件。- 你可以监听
Stream的listen方法,并在其onError回调中处理异常。 - 使用
await for循环时,可以直接在try-catch块中捕获Stream抛出的异常。
- 你可以监听
注意: 任何未捕获的Future异常最终都会被Zone捕获(如果没有设置自定义Zone,应用程序通常会终止)。
示例
// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM
// --- Future 异常处理 ---
/// 模拟一个可能失败的异步操作
Future<String> fetchData(bool shouldFail) async {
await Future.delayed(Duration(milliseconds: 500)); // 模拟网络延迟
if (shouldFail) {
throw Exception('数据获取失败:网络连接错误或服务器无响应');
}
return '成功获取到异步数据!';
}
/// 模拟另一个异步操作,可能返回特定错误
Future<int> processData(String rawData) async {
await Future.delayed(Duration(milliseconds: 300));
if (rawData.contains('error')) {
throw FormatException('数据格式不正确');
}
return rawData.length;
}
// --- Stream 异常处理 ---
/// 模拟一个可能抛出异常的 Stream
Stream<int> countWithErrors(int maxCount, {required bool shouldError}) async* {
for (int i = 0; i < maxCount; i++) {
await Future.delayed(Duration(milliseconds: 100));
if (i == 3 && shouldError) {
throw StateError('Stream 在第三次迭代时发生错误');
}
yield i;
}
}
Future<void> main() async {
print('--- Future 异常处理 (使用 await 和 try-catch) ---');
try {
String result = await fetchData(true); // 将会失败
print(result);
} on Exception catch (e) {
print('Future 1 捕获到异常 (await): ${e.runtimeType} - $e');
}
print('\n--- Future 异常处理 (使用 .catchError) ---');
fetchData(false).then((data) { // 将会成功
print('Future 2 获取数据成功: $data');
}).catchError((e) {
print('Future 2 捕获到异常 (catchError): ${e.runtimeType} - $e'); // 这不会被触发
});
fetchData(true).then((data) { // 将会失败,使用 .catchError
print('Future 3 获取数据成功: $data'); // 这不会被触发
}).catchError((e) {
print('Future 3 捕获到异常 (catchError): ${e.runtimeType} - $e');
}).whenComplete(() {
print('Future 3 处理完成 (无论成功失败)。'); // finally 块的异步版本
});
print('\n--- 组合 Future 异常处理 ---');
try {
String data = await fetchData(false);
int processedLength = await processData('valid data');
print('第一步数据: $data, 第二步处理长度: $processedLength');
data = await fetchData(false);
processedLength = await processData('error data'); // 将会抛出FormatException
print('第一步数据: $data, 第二步处理长度: $processedLength'); // 不会执行
} on FormatException catch (e) {
print('组合操作捕获到 FormatException: $e');
} on Exception catch (e) {
print('组合操作捕获到通用 Exception: $e');
}
print('\n--- Stream 异常处理 (使用 listen) ---');
countWithErrors(5, shouldError: true).listen(
(data) {
print('Stream listen 接收到数据: $data');
},
onError: (e, s) { // Stream 的 onError 回调
print('Stream listen 捕获到异常: ${e.runtimeType} - $e');
print('堆栈跟踪: $s');
},
onDone: () {
print('Stream listen 处理完成。');
},
cancelOnError: true, // 遇到错误时取消订阅
);
print('\n--- Stream 异常处理 (使用 await for 和 try-catch) ---');
try {
await for (var num in countWithErrors(5, shouldError: true)) {
print('await for 循环接收到数据: $num');
}
} on StateError catch (e, s) {
print('await for 循环捕获到 StateError: $e');
print('堆栈跟踪: $s');
} catch (e, s) {
print('await for 循环捕获到通用异常: ${e.runtimeType} - $e');
print('堆栈跟踪: $s');
}
await Future.delayed(Duration(seconds: 2)); // 等待所有异步操作完成
print('\n所有异步操作示例完成。');
}
运行结果
--- Future 异常处理 (使用 await 和 try-catch) ---
Future 1 捕获到异常 (await): Exception - Exception: 数据获取失败:网络连接错误或服务器无响应
--- Future 异常处理 (使用 .catchError) ---
Future 2 获取数据成功: 成功获取到异步数据!
Future 3 捕获到异常 (catchError): Exception - Exception: 数据获取失败:网络连接错误或服务器无响应
Future 3 处理完成 (无论成功失败)。
--- 组合 Future 异常处理 ---
第一步数据: 成功获取到异步数据!, 第二步处理长度: 10
组合操作捕获到 FormatException: FormatException: Bad format: 数据格式不正确
--- Stream 异常处理 (使用 listen) ---
Stream listen 接收到数据: 0
Stream listen 接收到数据: 1
Stream listen 接收到数据: 2
Stream listen 捕获到异常: StateError - Bad state: Stream 在第三次迭代时发生错误
堆栈跟踪: #0 countWithErrors (file:///.../main.dart:45:7)
#1 _StreamImpl.skipWhile.<anonymous closure>.<anonymous closure> (package:async/src/stream/stream_impl.dart:36:20)
#2 _StreamImpl._runGuarded (package:async/src/stream/stream_impl.dart:18:24)
#3 _DelayFuture._complete (dart:async/future_impl.dart:1033:23)
...
Stream listen 处理完成。
--- Stream 异常处理 (使用 await for 和 try-catch) ---
await for 循环接收到数据: 0
await for 循环接收到数据: 1
await for 循环接收到数据: 2
await for 循环捕获到 StateError: Bad state: Stream 在第三次迭代时发生错误
堆栈跟踪: #0 countWithErrors (file:///.../main.dart:46:7)
#1 main (file:///.../main.dart:110:17)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
所有异步操作示例完成。
注意事项
async/await与try-catch: 在async函数中使用await表达式时,其抛出的异常可以像同步代码一样被try-catch捕获。这是处理Future异常最推荐和最易读的方式。Future.catchError: 当不使用await或在链式调用Future时,catchError是捕获异常的有效方式。它有一个test参数可以用于过滤特定异常类型。Future.whenComplete: 类似于同步代码的finally块,whenComplete无论Future成功还是失败,都会执行。StreamController与addError: 如果你在创建Stream,可以使用StreamController.addError()方法来显式地向Stream发送一个错误事件。Zone的作用: 在Flutter或大型Dart应用中,Zone提供了一个机制来全局捕获未处理的异步异常,这对于错误报告和崩溃日志尤其有用。例如,Flutter的runZonedGuarded就是基于Zone提供的。
Error与Exception的区别
理论
在Dart中,Error和Exception都是用于表示程序中出现问题的类,但它们代表了不同类型的问题,并暗示了不同的处理策略。
-
Error(错误):- 通常表示程序中的逻辑缺陷或运行时不变量被违反。
- 这类错误通常是程序员的错误,例如:
AssertionError(断言失败)NullThrownError(抛出了null)StackOverflowError(栈溢出)OutOfMemoryError(内存不足)ArgumentError(方法接收到非法参数)StateError(对象处于非法状态,例如在一个已关闭的流上写入)
Error通常是不可恢复的。这意味着我们不应该尝试catch Error并继续执行应用程序。如果一个Error发生,通常程序应该终止,并通过修复代码来解决问题。- 继承自
Error的类通常不实现Exception接口。
-
Exception(异常):- 通常表示程序在运行时遇到的、预期可能发生的、但又不属于正常流程的意外情况。
- 这类情况通常是程序外部因素导致的,例如:
- 网络连接失败
- 文件找不到
- 用户的无效输入
- JSON解析失败(FormatException)
Exception通常是可恢复的。这意味着你可以catch Exception并采取适当的措施(如重试操作、向用户显示错误消息、记录日志等),然后程序可以继续执行。Exception类实现了Exception接口(在Dart中,Exception本身就是基类,不是接口)。
| 特征 | Error | Exception |
|---|---|---|
| 含义 | 程序内部的编程错误或系统资源耗尽 | 程序可预见的、可恢复的运行时问题 |
| 原因 | 程序员的逻辑错误、内存不足、栈溢出等 | 网络问题、文件不存在、无效的用户输入、API返回错误等 |
| 恢复性 | 不可恢复,通常表示程序需要终止并修复代码 | 可恢复,可以捕获并优雅处理,程序可继续运行 |
| 例子 | ArgumentError、StateError、NullThrownError | FormatException、IOException、TimeoutException |
| 处理策略 | 不应捕获,应终止程序并进行代码调试 | 应捕获并处理以保持程序健壮性 |
示例
// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM
// 模拟函数,可能抛出 ArgumentError (Error的子类)
void processValue(Object? value) {
if (value == null) {
// 这是一个典型的编程错误:不应该将 null 传入需要非空值的地方
throw ArgumentError('值不能为 null。'); // ArgumentError 是 Error 的子类
}
if (!(value is String)) {
// 假设此函数只处理字符串
throw ArgumentError('期望字符串类型,收到 ${value.runtimeType}。');
}
print('处理值成功: $value');
}
// 模拟函数,可能抛出 FormatException (Exception的子类)
void parseNumber(String text) {
try {
int number = int.parse(text); // 可能会抛出 FormatException
print('解析数字成功: $number');
} on FormatException catch (e) {
// 这是一个可预期的用户输入错误,可以捕获并进行处理
print('捕获到 FormatException (可恢复): $e - 输入 "$text" 不是有效的数字格式。');
// 可以进行重试、提示用户重新输入等操作
}
}
void main() {
print('--- 处理值 (ArgumentError - 不应捕获) ---');
try {
processValue('hello');
processValue(123); // 会抛出 ArgumentError (期望 String)
} catch (e) {
// 虽然这里捕获了,但在实际生产代码中,不应为了恢复而捕获 ArgumentError
print('捕获到异常: ${e.runtimeType} - $e');
}
print('\n--- 解析数字 (FormatException - 应捕获并恢复) ---');
parseNumber('123');
parseNumber('abc'); // 会抛出 FormatException,但我们捕获并处理了
parseNumber('45.6'); // 也会抛出 FormatException
// 演示未捕获的 Error 导致程序终止
print('\n--- 演示未捕获的 Error (将会使程序终止) ---');
try {
List<int> numbers = [1, 2, 3];
print(numbers[5]); // IndexError 是 Error 的子类,通常不应该捕获
} catch (e) {
print('捕获了 Index Error (但通常不建议这样做): ${e.runtimeType} - $e');
// 即使在这里捕获,也表明程序存在逻辑错误,需要修复
}
// 如果这里再 `throw IndexError('...')` 并且不捕获,程序就会终止
print('\n程序执行完毕。'); // 如果有未捕获的 Error,这行可能不会被执行
}
运行结果
--- 处理值 (ArgumentError - 不应捕获) ---
处理值成功: hello
捕获到异常: ArgumentError - Invalid argument(s): 期望字符串类型,收到 int。
--- 解析数字 (FormatException - 应捕获并恢复) ---
解析数字成功: 123
捕获到 FormatException (可恢复): FormatException: Invalid number (at character 1)
abc
^
- 输入 "abc" 不是有效的数字格式。
捕获到 FormatException (可恢复): FormatException: Invalid number (at character 3)
45.6
^
- 输入 "45.6" 不是有效的数字格式。
--- 演示未捕获的 Error (将会使程序终止) ---
捕获了 Index Error (但通常不建议这样做): RangeError - RangeError (index): Invalid value: Not in inclusive range 0..2: 5
程序执行完毕。
如果上面numbers[5]的try-catch被移除,或者没有捕获RangeError,则程序的输出会是
--- 处理值 (ArgumentError - 不应捕获) ---
处理值成功: hello
捕获到异常: ArgumentError - Invalid argument(s): 期望字符串类型,收到 int。
--- 解析数字 (FormatException - 应捕获并恢复) ---
解析数字成功: 123
捕获到 FormatException (可恢复): FormatException: Invalid number (at character 1)
abc
^
- 输入 "abc" 不是有效的数字格式。
捕获到 FormatException (可恢复): FormatException: Invalid number (at character 3)
45.6
^
- 输入 "45.6" 不是有效的数字格式。
--- 演示未捕获的 Error (将会使程序终止) ---
Unhandled exception:
RangeError (index): Invalid value: Not in inclusive range 0..2: 5
#0 List.[] (dart:core-patch/growable_array.dart:257:3)
#1 main (file:///.../main.dart:58:18)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
可以看到,当RangeError未被捕获时,程序直接终止。
注意事项
- 不要轻易捕获
Error: 通常不建议使用try-catch来捕获Error及其子类,因为它们表明程序存在缺陷,捕获它们会掩盖真正的问题,并可能导致程序处于不确定状态。正确的做法是修复代码以避免这些Error发生。 - 捕获
Exception是为了恢复: 捕获Exception是为了处理预期的运行时问题,并允许程序从这些问题中恢复或优雅地失败。 - 如何区分
Error和Exception: 当你遇到一个问题时,思考它是否是由于你的代码逻辑错误导致的(例如不正确的参数传递、无效状态),如果是,那它更偏向于Error。如果它是由于外部环境或用户行为导致的(例如网络断开、文件权限不足、用户输入格式不正确),那它更偏向于Exception。 AssertionError: Dart的assert语句会在开发模式下抛出AssertionError。在生产模式下,assert语句会被忽略,不会抛出异常。
实践指南与最佳实践
理论
良好地处理异常是构建健壮应用程序的关键。以下是一些在Dart中处理异常的实践指南和最佳实践:
- 只捕获你知道如何处理的异常: 不要无差别地捕获所有异常(
catch (e)),除非你真的知道如何处理所有这些异常,或者你只是想记录它们。更具体的on语句通常更好。 - 避免空
catch块: 一个空的catch块会吞噬异常,使你无法知道发生了什么问题,并且难以调试。至少应该记录异常或重新抛出它。 - 重新抛出异常(
rethrow): 如果你捕获了一个异常,但发现自己无法完全处理它,或者希望上层调用者也能知道这个异常,你可以使用rethrow关键字。rethrow会重新抛出原始异常,并保留其原始堆栈跟踪。 - 提供有意义的错误信息: 无论是在抛出自定义异常还是记录内置异常时,确保错误消息清晰、具体,并包含足够的信息来诊断问题。
- 为用户提供友好反馈: 在用户界面层,将技术性异常转换为用户友好的消息,告知用户发生了什么,以及他们可以尝试什么(如果可以)。
- 日志记录: 始终将捕获到的异常及其堆栈跟踪记录到日志系统,特别是在生产环境中。这对于后期分析和调试至关重要。
- 异步异常的
Zone处理: 在Flutter或大型Dart应用中,使用runZonedGuarded来全局捕获所有未处理的异步异常,防止应用崩溃,并集中处理错误报告。 - 考虑使用
Result类型(函数式编程风格): 对于某些场景,特别是纯函数,与其抛出异常,不如返回一个表示成功值或失败错误的Result对象。这使得函数签名清晰地表明了两种可能的返回状态,而不会中断正常的控制流。这是一种替代异常的错误处理方式,尤其适用于库或API设计。
示例
// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM
import 'dart:io';
// 模拟一个依赖于文件读取的函数
Future<String> readFileContent(String filePath) async {
try {
File file = File(filePath);
if (!await file.exists()) {
// 文件不存在是预期但需要处理的场景,抛出自定义异常更合适
throw FileSystemException('文件不存在', filePath);
}
String content = await file.readAsString();
if (content.isEmpty) {
throw FileSystemException('文件内容为空', filePath);
}
return content;
} on FileSystemException catch (e, s) {
// 捕获特定的文件系统异常,并添加更多上下文信息
print('[LOG] 警告: 文件系统操作失败,路径: $filePath - ${e.message}');
// 重新抛出,让上层知道这个异常
rethrow;
} on PathNotFoundException catch (e) {
// 针对PathNotFoundException这种更具体的异常进行处理
print('[LOG] 错误: 指定路径找不到 - ${e.message}');
rethrow;
} catch (e, s) {
// 捕获所有其他未知异常,并记录详细信息
print('[LOG] 严重错误: 读取文件时发生未知异常: ${e.runtimeType} - $e');
print('[LOG] 堆栈跟踪: $s');
rethrow; // 重新抛出,如果这里不处理,让上层决定如何应对
} finally {
print('文件读取尝试完成 ($filePath)。');
}
}
// Result 类型示例 (函数式错误处理,替代异常)
// 定义一个 Result 类型,表示成功或失败
sealed class Result<T, E> {
const Result();
}
class Success<T, E> extends Result<T, E> {
final T value;
const Success(this.value);
}
class Failure<T, E> extends Result<T, E> {
final E error;
const Failure(this.error);
}
/// 模拟一个安全解析整数的函数,返回 Result 类型
Result<int, String> safeParseInt(String input) {
try {
int value = int.parse(input);
return Success(value);
} on FormatException {
return Failure('输入 "$input" 不是有效的整数格式。');
}
}
void main() async {
// 1. 合理使用 try-on-catch-finally + 日志记录
print('--- 尝试读取一个存在的文件 ---');
File('existing_file.txt').writeAsString('Hello Dart!').then((_) async {
try {
String content = await readFileContent('existing_file.txt');
print('成功读取内容: "$content"');
} catch (e) {
print('主函数中捕获到文件读取异常: ${e.runtimeType} - $e');
// 可以给用户显示一个友好的错误提示
print('提示用户: 无法加载文件内容,请稍后重试。');
}
});
await Future.delayed(Duration(milliseconds: 100)); // 等待文件操作完成
print('\n--- 尝试读取一个不存在的文件 ---');
try {
String content = await readFileContent('non_existent_file.txt');
print('成功读取内容: "$content"'); // 不会执行
} catch (e) {
print('主函数中捕获到文件读取异常: ${e.runtimeType} - $e');
print('提示用户: 文件不存在,请检查路径。');
}
print('\n--- 尝试读取一个空文件 ---');
File('empty_file.txt').writeAsString('').then((_) async {
try {
String content = await readFileContent('empty_file.txt');
print('成功读取内容: "$content"'); // 不会执行
} catch (e) {
print('主函数中捕获到文件读取异常: ${e.runtimeType} - $e');
print('提示用户: 文件内容为空。');
}
});
await Future.delayed(Duration(milliseconds: 100)); // 等待文件操作完成
// 2. Result 类型演示
print('\n--- 使用 Result 类型进行错误处理 ---');
var num1Result = safeParseInt('123');
if (num1Result is Success<int, String>) {
print('成功解析数字: ${num1Result.value}');
} else if (num1Result is Failure<int, String>) {
print('解析数字失败: ${num1Result.error}');
}
var num2Result = safeParseInt('hello');
if (num2Result is Success<int, String>) {
print('成功解析数字: ${num2Result.value}');
} else if (num2Result is Failure<int, String>) {
print('解析数字失败: ${num2Result.error}');
}
await Future.delayed(Duration(seconds: 1)); // 等待所有异步操作完成
print('\n程序执行完毕。');
}
运行结果
--- 尝试读取一个存在的文件 ---
文件读取尝试完成 (existing_file.txt)。
成功读取内容: "Hello Dart!"
--- 尝试读取一个不存在的文件 ---
[LOG] 警告: 文件系统操作失败,路径: non_existent_file.txt - 文件不存在
文件读取尝试完成 (non_existent_file.txt)。
主函数中捕获到文件读取异常: FileSystemException - FileSystemException: FileSystemException: 文件不存在, path = 'non_existent_file.txt'
提示用户: 文件不存在,请检查路径。
--- 尝试读取一个空文件 ---
[LOG] 警告: 文件系统操作失败,路径: empty_file.txt - 文件内容为空
文件读取尝试完成 (empty_file.txt)。
主函数中捕获到文件读取异常: FileSystemException - FileSystemException: FileSystemException: 文件内容为空, path = 'empty_file.txt'
提示用户: 文件内容为空。
--- 使用 Result 类型进行错误处理 ---
成功解析数字: 123
解析数字失败: 输入 "hello" 不是有效的整数格式。
程序执行完毕。
注意事项
- 过度捕获的危害: 捕获过于宽泛的异常类型(例如直接
catch (e)而不加on)可能会隐藏程序中的真正问题,导致难以发现和修复bug。 - 资源泄漏: 在处理资源(如文件、网络连接)时,务必使用
finally块或Future.whenComplete来确保资源被正确释放,即使发生异常。 - 错误边界: 在大型应用中,考虑在更高级别的组件或服务中建立“错误边界”,集中处理来自其子组件或依赖服务的异常,以防止整个应用程序崩溃。例如,在Flutter中使用
ErrorWidget.builder或runZonedGuarded。 Result类型与异常的选择:Result类型适用于函数可能预期地返回多种状态(成功值或特定错误)的场景,它强制调用者处理所有可能的输出。异常更适用于表示不应该发生的、非预期的错误,或那些会中断正常流程的严重问题。在设计API时,根据错误的可预测性和可恢复性来选择。