使用 Result 物件進行錯誤處理
Dart 提供了一個內建的錯誤處理機制,能夠丟擲和捕獲異常。
如 錯誤處理文件中所述,Dart 的異常是未處理的異常。這意味著丟擲異常的方法不需要宣告它們,呼叫方法也不需要捕獲它們。
這可能導致異常未得到妥善處理的情況。在大型專案中,開發者可能會忘記捕獲異常,而不同的應用層和元件可能會丟擲未記錄的異常。這可能導致錯誤和崩潰。
在本指南中,您將瞭解此限制以及如何使用 *Result* 模式來緩解此問題。
Flutter 應用中的錯誤流程
#遵循 Flutter 架構指南的應用通常由 ViewModel、Repository 和 Service 等部分組成。當這些元件中的函式失敗時,它應該將錯誤通知給呼叫元件。
通常,這是透過異常來完成的。例如,API 客戶端服務在與遠端伺服器通訊失敗時可能會丟擲 `HttpError` 異常。呼叫元件,例如 Repository,將必須捕獲此異常,或忽略它並讓呼叫 ViewModel 來處理。
這可以在以下示例中觀察到。考慮這些類:
- 一個服務 `ApiClientService`,它執行對遠端服務的 API 呼叫。
- 一個 Repository `UserProfileRepository`,它提供由 `ApiClientService` 提供的 `UserProfile`。
- 一個 ViewModel `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 Client 的值。
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` 來解決此問題,警告其可能丟擲的異常。但是,由於 ViewModel 不直接使用該服務,因此程式碼庫中的其他開發者可能會忽略此資訊。
使用 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` 的 `Result` 物件,而不是直接返回 `UserProfile`。
為了方便使用 `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` 中。
Repository 類也需要修改,不再直接返回 `UserProfile`,而是返回 `Result<UserProfile>`。
Future<Result<UserProfile>> getUserProfile() async {
return await _apiClientService.getUserProfile();
}解包 Result 物件
#現在 ViewModel 不直接接收 `UserProfile`,而是接收一個包含 `UserProfile` 的 `Result`。
這強制實現 ViewModel 的開發者解包 `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` 類的完整實現,該實現是在 Compass App 示例中為 Flutter 架構指南實現的。
/// 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)';
}