Skip to main content

使用 Result 对象进行错误处理

Dart 提供了内置的错误处理机制,能够抛出和捕获异常。

错误处理文档中所述,Dart 的异常是未处理的异常。这意味着抛出异常的方法不需要声明它们,调用方法也不需要捕获它们。

这可能导致异常未被正确处理的情况。在大项目中,开发人员可能会忘记捕获异常,不同的应用程序层和组件可能会抛出未记录的异常。这可能导致错误和崩溃。

在本指南中,您将了解此限制以及如何使用 result 模式来减轻它。

Flutter 应用中的错误流

#

遵循Flutter 架构指南 的应用程序通常由视图模型、存储库和服务等部分组成。当这些组件中的一个函数失败时,它应该将错误传达给调用组件。

通常,这是通过异常来完成的。例如,API 客户端服务未能与远程服务器通信可能会抛出 HTTP 错误异常。调用组件(例如存储库)必须捕获此异常或忽略它并让调用视图模型处理它。

这可以在以下示例中观察到。考虑以下类:

  • 服务 ApiClientService 执行对远程服务的 API 调用。
  • 存储库 UserProfileRepository 提供由 ApiClientService 提供的 UserProfile
  • 视图模型 UserProfileViewModel 使用 UserProfileRepository

ApiClientService 包含一个方法 getUserProfile,在某些情况下会抛出异常:

  • 如果响应代码不是 200,则该方法会抛出 HttpException
  • 如果响应格式不正确,则 JSON 解析方法会抛出异常。
  • HTTP 客户端可能会由于网络问题而抛出异常。

以下代码测试各种可能的异常:

dart
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 客户端的值。

dart
class UserProfileRepository {
  // ···

  Future<UserProfile> getUserProfile() async {
    return await _apiClientService.getUserProfile();
  }
}

最后,UserProfileViewModel 应该捕获所有异常并处理错误。

这可以通过用 try-catch 包装对 UserProfileRepository 的调用来完成:

dart
class UserProfileViewModel extends ChangeNotifier {
  // ···

  Future<void> load() async {
    try {
      _userProfile = await userProfileRepository.getUserProfile();
      notifyListeners();
    } on Exception catch (exception) {
      // handle exception
    }
  }
}

实际上,开发人员可能会忘记正确捕获异常并最终得到以下代码。它可以编译和运行,但是如果发生前面提到的任何异常,它就会崩溃:

dart
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 类的示例,为了演示目的,它已被简化。完整的实现位于此页的末尾。

dart
/// 简化错误处理的实用程序类。
///
/// 从函数返回 [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 来表示任何返回值,它可以是像 Stringint 这样的 Dart 原生类型,也可以是像 UserProfile 这样的自定义类。

创建 Result 对象

#

对于使用 Result 类返回值的函数,函数不返回值,而是返回包含值的 Result 对象。

例如,在 ApiClientService 中,getUserProfile 被更改为返回 Result

dart
class ApiClientService {
  // ···

  Future<Result<UserProfile>> getUserProfile() async {
    // ···
  }
}

它不直接返回 UserProfile,而是返回包含 UserProfileResult 对象。

为了方便使用 Result 类,它包含两个命名构造函数 Result.okResult.error。根据所需的输出使用它们来构造 Result。同样,捕获代码抛出的任何异常并将其包装到 Result 对象中。

例如,这里 getUserProfile() 方法已更改为使用 Result 类:

dart
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>

dart
Future<Result<UserProfile>> getUserProfile() async {
  return await _apiClientService.getUserProfile();
}

解包 Result 对象

#

现在视图模型不直接接收 UserProfile,而是接收包含 UserProfileResult

这强制实现视图模型的开发人员解包 Result 以获得 UserProfile,并避免出现未捕获的异常。

dart
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 类实现,这意味着它只能是 OkError 类型。这允许代码使用 switch 结果或表达式 来评估结果。

Ok<UserProfile> 的情况下,使用 value 属性获取值。

Error<UserProfile> 的情况下,使用 error 属性获取错误对象。

改善控制流

#

将代码包装在 try-catch 块中可确保捕获抛出的异常,并且不会将其传播到代码的其他部分。

考虑以下代码。

dart
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 模式来改进:

dart
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 的,只能返回 OkError 实例,允许代码使用 switch 语句解包它们。

您可以在下面找到在指南针应用程序示例中实现的完整的 Result 类,用于Flutter 架构指南