使用 Result 对象进行错误处理
Dart 提供内置的错误处理机制,能够抛出和捕获异常。
如《错误处理文档》中所述,Dart 的异常是未处理的异常。这意味着抛出异常的方法无需声明它们,调用方法也不需要捕获它们。
这可能导致异常未被正确处理的情况。在大型项目中,开发者可能会忘记捕获异常,并且不同的应用程序层和组件可能会抛出未被文档记录的异常。这可能导致错误和崩溃。
在本指南中,你将了解此限制以及如何使用 Result 模式 来缓解它。
Flutter 应用程序中的错误流
#遵循Flutter 架构指南的应用程序通常由视图模型 (view models)、仓库 (repositories) 和服务 (services) 等部分组成。当其中一个组件中的函数失败时,它应该将错误传达给调用组件。
通常,这是通过异常来完成的。例如,一个无法与远程服务器通信的 API 客户端服务可能会抛出一个 HTTP 错误异常 (HTTP Error Exception)。调用组件,例如一个仓库 (Repository),必须捕获此异常,或者忽略它并让调用视图模型 (view model) 处理它。
这可以在以下示例中观察到。考虑以下类:
- 一个服务 `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` 应该捕获所有异常并处理错误。
这可以通过将对 `UserProfileRepository` 的调用包装在 try-catch 块中来完成。
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` 类示例。完整的实现可在本页末尾找到。
/// Utility class that simplifies handling errors.
///
/// Return a [Result] from a function to indicate success or failure.
///
/// A [Result] is either an [Ok] with a value of type [T]
/// or an [Error] with an [Exception].
///
/// Use [Result.ok] to create a successful result with a value of type [T].
/// Use [Result.error] to create an error result with an [Exception].
sealed class Result<T> {
const Result();
/// Creates an instance of Result containing a value
factory Result.ok(T value) => Ok(value);
/// Create an instance of Result containing an error
factory Result.error(Exception error) => Error(error);
}
/// Subclass of Result for values
final class Ok<T> extends Result<T> {
const Ok(this.value);
/// Returned value in result
final T value;
}
/// Subclass of Result for errors
final class Error<T> extends Result<T> {
const Error(this.error);
/// Returned error in result
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();
}
}
}
原始的返回语句被替换为使用 `Result.ok` 返回值的语句。`throw HttpException()` 被替换为返回 `Result.error(HttpException())` 的语句,将错误包装到 `Result` 中。此外,该方法被 `try-catch` 块包装,以捕获 HTTP 客户端或 JSON 解析器抛出的任何异常并将其封装到 `Result.error` 中。
仓库类也需要修改,它不再直接返回 `UserProfile`,而是返回 `Result
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
在 `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 语句解包它们。
下面你可以找到在《指南针应用示例》中为Flutter 架构指南实现的完整 `Result` 类。
/// Utility class that simplifies handling errors.
///
/// Return a [Result] from a function to indicate success or failure.
///
/// A [Result] is either an [Ok] with a value of type [T]
/// or an [Error] with an [Exception].
///
/// Use [Result.ok] to create a successful result with a value of type [T].
/// Use [Result.error] to create an error result with an [Exception].
///
/// Evaluate the result using a switch statement:
/// ```dart
/// switch (result) {
/// case Ok(): {
/// print(result.value);
/// }
/// case Error(): {
/// print(result.error);
/// }
/// }
/// ```
sealed class Result<T> {
const Result();
/// Creates a successful [Result], completed with the specified [value].
const factory Result.ok(T value) = Ok._;
/// Creates an error [Result], completed with the specified [error].
const factory Result.error(Exception error) = Error._;
}
/// A successful [Result] with a returned [value].
final class Ok<T> extends Result<T> {
const Ok._(this.value);
/// The returned value of this result.
final T value;
@override
String toString() => 'Result<$T>.ok($value)';
}
/// An error [Result] with a resulting [error].
final class Error<T> extends Result<T> {
const Error._(this.error);
/// The resulting error of this result.
final Exception error;
@override
String toString() => 'Result<$T>.error($error)';
}