使用 Result 对象进行错误处理
Dart 提供了内置的错误处理机制,能够抛出和捕获异常。
如错误处理文档中所述,Dart 的异常是未处理的异常。这意味着抛出异常的方法不需要声明它们,调用方法也不需要捕获它们。
这可能导致异常未被正确处理的情况。在大项目中,开发人员可能会忘记捕获异常,不同的应用程序层和组件可能会抛出未记录的异常。这可能导致错误和崩溃。
在本指南中,您将了解此限制以及如何使用 result 模式来减轻它。
Flutter 应用中的错误流
#遵循Flutter 架构指南 的应用程序通常由视图模型、存储库和服务等部分组成。当这些组件中的一个函数失败时,它应该将错误传达给调用组件。
通常,这是通过异常来完成的。例如,API 客户端服务未能与远程服务器通信可能会抛出 HTTP 错误异常。调用组件(例如存储库)必须捕获此异常或忽略它并让调用视图模型处理它。
这可以在以下示例中观察到。考虑以下类:
- 服务
ApiClientService执行对远程服务的 API 调用。 - 存储库
UserProfileRepository提供由ApiClientService提供的UserProfile。 - 视图模型
UserProfileViewModel使用UserProfileRepository。
ApiClientService 包含一个方法 getUserProfile,在某些情况下会抛出异常:
- 如果响应代码不是 200,则该方法会抛出
HttpException。 - 如果响应格式不正确,则 JSON 解析方法会抛出异常。
- HTTP 客户端可能会由于网络问题而抛出异常。
以下代码测试各种可能的异常:
class ApiClientService {
// ···
Future<UserProfile> getUserProfile() async {
try {
final request = await client.get(_host, _port, '/user');
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
return UserProfile.fromJson(jsonDecode(stringData));
} else {
throw const HttpException('Invalid response');
}
} finally {
client.close();
}
}
}UserProfileRepository 不需要处理来自 ApiClientService 的异常。在这个例子中,它只是返回 API 客户端的值。
class UserProfileRepository {
// ···
Future<UserProfile> getUserProfile() async {
return await _apiClientService.getUserProfile();
}
}最后,UserProfileViewModel 应该捕获所有异常并处理错误。
这可以通过用 try-catch 包装对 UserProfileRepository 的调用来完成:
class UserProfileViewModel extends ChangeNotifier {
// ···
Future<void> load() async {
try {
_userProfile = await userProfileRepository.getUserProfile();
notifyListeners();
} on Exception catch (exception) {
// handle exception
}
}
}实际上,开发人员可能会忘记正确捕获异常并最终得到以下代码。它可以编译和运行,但是如果发生前面提到的任何异常,它就会崩溃:
class UserProfileViewModel extends ChangeNotifier {
// ···
Future<void> load() async {
_userProfile = await userProfileRepository.getUserProfile();
notifyListeners();
}
}您可以尝试通过记录 ApiClientService 来解决此问题,警告它可能抛出的可能的异常。但是,由于视图模型不直接使用服务,因此在代码库中工作的其他开发人员可能会错过此信息。
使用 result 模式
#抛出异常的替代方法是用 Result 对象包装函数输出。
当函数成功运行时,Result 包含返回值。但是,如果函数未成功完成,Result 对象包含错误。
Result 是一个 sealed 类,它可以是 Ok 子类或 Error 类。使用子类 Ok 返回成功值,使用子类 Error 返回捕获的错误。
以下代码显示了一个 Result 类的示例,为了演示目的,它已被简化。完整的实现位于此页的末尾。
/// 简化错误处理的实用程序类。
///
/// 从函数返回 [Result] 以指示成功或失败。
///
/// [Result] 要么是包含类型为 [T] 的值的 [Ok],
/// 要么是包含 [Exception] 的 [Error]。
///
/// 使用 [Result.ok] 创建一个包含类型为 [T] 的值的成功结果。
/// 使用 [Result.error] 创建一个包含 [Exception] 的错误结果。
sealed class Result<T> {
const Result();
/// 创建包含值的 Result 实例
factory Result.ok(T value) => Ok(value);
/// 创建包含错误的 Result 实例
factory Result.error(Exception error) => Error(error);
}
/// 值的 Result 子类
final class Ok<T> extends Result<T> {
const Ok(this.value);
/// 结果中的返回值
final T value;
}
/// 错误的 Result 子类
final class Error<T> extends Result<T> {
const Error(this.error);
/// 结果中的返回错误
final Exception error;
}在此示例中,Result 类使用泛型类型 T 来表示任何返回值,它可以是像 String 或 int 这样的 Dart 原生类型,也可以是像 UserProfile 这样的自定义类。
创建 Result 对象
#对于使用 Result 类返回值的函数,函数不返回值,而是返回包含值的 Result 对象。
例如,在 ApiClientService 中,getUserProfile 被更改为返回 Result:
class ApiClientService {
// ···
Future<Result<UserProfile>> getUserProfile() async {
// ···
}
}它不直接返回 UserProfile,而是返回包含 UserProfile 的 Result 对象。
为了方便使用 Result 类,它包含两个命名构造函数 Result.ok 和 Result.error。根据所需的输出使用它们来构造 Result。同样,捕获代码抛出的任何异常并将其包装到 Result 对象中。
例如,这里 getUserProfile() 方法已更改为使用 Result 类:
class ApiClientService {
// ···
Future<Result<UserProfile>> getUserProfile() async {
try {
final request = await client.get(_host, _port, '/user');
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
return Result.ok(UserProfile.fromJson(jsonDecode(stringData)));
} else {
return const Result.error(HttpException('Invalid response'));
}
} on Exception catch (exception) {
return Result.error(exception);
} finally {
client.close();
}
}
}原始的 return 语句被替换为使用 Result.ok 返回值的语句。throw HttpException() 被替换为返回 Result.error(HttpException()) 的语句,将错误包装到 Result 中。同样,该方法被 try-catch 块包装起来,以将 HTTP 客户端或 JSON 解析器抛出的任何异常捕获到 Result.error 中。
存储库类也需要修改,它不再直接返回 UserProfile,而是返回 Result<UserProfile>。
Future<Result<UserProfile>> getUserProfile() async {
return await _apiClientService.getUserProfile();
}解包 Result 对象
#现在视图模型不直接接收 UserProfile,而是接收包含 UserProfile 的 Result。
这强制实现视图模型的开发人员解包 Result 以获得 UserProfile,并避免出现未捕获的异常。
class UserProfileViewModel extends ChangeNotifier {
// ···
UserProfile? userProfile;
Exception? error;
Future<void> load() async {
final result = await userProfileRepository.getUserProfile();
switch (result) {
case Ok<UserProfile>():
userProfile = result.value;
case Error<UserProfile>():
error = result.error;
}
notifyListeners();
}
}Result 类使用 sealed 类实现,这意味着它只能是 Ok 或 Error 类型。这允许代码使用 switch 结果或表达式 来评估结果。
在 Ok<UserProfile> 的情况下,使用 value 属性获取值。
在 Error<UserProfile> 的情况下,使用 error 属性获取错误对象。
改善控制流
#将代码包装在 try-catch 块中可确保捕获抛出的异常,并且不会将其传播到代码的其他部分。
考虑以下代码。
class UserProfileRepository {
// ···
Future<UserProfile> getUserProfile() async {
try {
return await _apiClientService.getUserProfile();
} catch (e) {
try {
return await _databaseService.createTemporaryUser();
} catch (e) {
throw Exception('Failed to get user profile');
}
}
}
}在此方法中,UserProfileRepository 尝试使用 ApiClientService 获取 UserProfile。如果失败,它尝试在 DatabaseService 中创建一个临时用户。
因为任一服务方法都可能失败,所以代码必须在这两种情况下都捕获异常。
这可以使用 Result 模式来改进:
Future<Result<UserProfile>> getUserProfile() async {
final apiResult = await _apiClientService.getUserProfile();
if (apiResult is Ok) {
return apiResult;
}
final databaseResult = await _databaseService.createTemporaryUser();
if (databaseResult is Ok) {
return databaseResult;
}
return Result.error(Exception('Failed to get user profile'));
}在此代码中,如果 Result 对象是 Ok 实例,则函数返回该对象;否则,它返回 Result.Error。
将所有内容整合在一起
#在本指南中,您学习了如何使用 Result 类返回结果值。
主要要点是:
Result类强制调用方法检查错误,减少了由未捕获的异常引起的错误数量。- 与 try-catch 块相比,
Result类有助于改善控制流。 Result类是sealed的,只能返回Ok或Error实例,允许代码使用 switch 语句解包它们。
您可以在下面找到在指南针应用程序示例中实现的完整的 Result 类,用于Flutter 架构指南。
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。