每个Dart开发者都曾在某个时刻写过这样的代码:
try {
final user = await repository.getUser(id);
// 对user进行操作
} catch (e) {
// e到底是什么?谁知道呢。
print(e.toString());
}
这段代码可以正常运行,也能通过编译,最终被部署到生产环境中。然而六个月后,会有一位用户向你发送错误报告,他看到的只是空白屏幕,而没有任何错误信息。你需要花费三个小时来追踪问题,最终发现是某个catch (e)块悄悄地掩盖了程序出现的故障。
这就是Dart中基于异常的错误处理机制所存在的根本性问题:在函数签名中,异常是无法被看到的;在调用时,异常也不携带任何类型信息;编译器也无法提供帮助,因为它根本不知道某个函数可能会失败。
实际上,每一个可能导致程序失败的路径,都体现了函数编写者与调用者之间的一种“社会契约”。然而,在压力巨大的团队环境中,或者在凌晨两点发生紧急情况时,这种“社会契约”往往就会被打破。
生产环境中的应用程序显然应该获得更好的错误处理机制。
在本文中,我们将详细介绍一种现代的、适用于Dart的错误处理方法——那种真正被用于实际生产环境的Flutter代码库中的错误处理方式。我们会从使用Dart Records作为轻量级的结果容器开始讲起,接着构建一个合适的、不可修改的结果类型,将其扩展为Monad模式,集成
通过这些方法,你代码库中的所有错误都将被明确地标记出来,编译器也会强制要求开发者对这些错误进行处理,因此绝对不可能忽视它们。
目录
先决条件
在开始之前,您需要具备以下条件:
-
一个使用Dart 3.0或更高版本的可用Flutter项目
-
对Dart的泛型以及async/await机制有基本了解
-
对Dart中的密封类有基本认识
-
已经安装了
freezed、freezed.annotation和build_runner这三个包 -
已经安装了
dartz包 -
在您的项目中,命令
flutter pub run buildrunner build能够正常执行
Dart中异常处理存在的问题
让我们看看,在一个完整的开发流程中,典型的基于异常的错误处理机制实际上是怎样的:
// 数据库层
Future getUser(String id) async {
final response = await dio.get('/users/$id');
return User.fromJson(response.data);
}
// 使用层
Future execute(String id) async {
return await repository.getUser(id);
}
// 视图模型层
Future loadUser(String id) async {
try {
final user = await useCase.execute(id);
state = UserState.loaded(user);
} catch (e) {
state = UserState.error(e.toString());
}
}
这种设计看起来似乎很合理,但实际上其中隐藏着严重的问题。
从函数签名上看,失败情况是无法被察觉的: 函数签名Future告诉调用者“你会得到一个User对象”,但并没有说明当网络请求失败、令牌过期或JSON数据格式不正确时会发生什么。调用者只有通过阅读代码实现才能知道这个函数可能会失败。
编译器也无法帮助你: 如果你在视图模型层忘记了编写try/catch>语句,应用程序仍然能够正常编译通过。但问题会在运行时出现,而且是在真实用户面前发生的。
catch (e)这个语句会捕获所有类型的错误:变量名拼写错误、空指针异常、真正的网络请求失败——所有这些错误都会被统一处理到同一个catch块中。如果不查看具体的错误信息,就无法区分这些错误的类型,而这种做法本身就非常不可靠。
错误在传递过程中会丢失其类型信息: 当API层抛出的UnauthorizedException到达视图模型层时,它就已经变成了一个普通的Object对象,所有的结构化信息都消失了。
解决这个问题的方法就是:让错误成为函数签名、类型系统以及编译器检查中不可或缺的一部分。本文介绍的各种模式正是为了实现这一目标而设计的。
第1部分:将记录类型作为轻量级的结果容器
什么是Dart记录类型?
Dart 3.0引入了记录类型——这种匿名且不可变的值类型允许人们将多个字段组合在一起,而无需编写完整的类定义。
// 一个包含两个命名字段的记录类型
({String name, int age}) person = (name: 'Seyi', age: 28);
print(person.name); // 输出:Seyi
print(person.age); // 输出:28
记录具有结构化类型特性——只要两个记录的字段名称和类型相同,那么它们就属于同一类型,无论这些字段是在哪里定义的。此外,记录是不可变的,并且是通过值来进行比较的,而不是通过引用。
记录作为结果类型
在错误处理中,记录最简单的应用方式就是将成功与失败编码为一种包含可空字段的单一返回类型:
typedef Result = ({E? e, T? data});
这种定义创建了一种包含两个可空字段的记录类型:e用于表示错误信息,data用于表示成功结果。其规则很简单:这两个字段中必定只有一个是非空的。
// 成功时——数据存在,e为null
Result result = (e: null, data: user);
// 失败时——e存在,data为null
Result result = (e: '用户未找到', data: null);
与异常处理方式相比,这种设计已经有了显著的改进。返回类型明确告诉调用者:该函数可能会返回数据,也可能会返回错误信息。失败情况本身就是该函数接口的一部分。
你还可以为应用程序的不同层次定义更具体的类型别名:
typedef ApiResult = ({T? data, E? exception});
typedef SecurityResponse = {bool? isSecured, String? error};
typedef Repository = ApiResult;
每个类型别名都为这种记录结构赋予了一个有意义的名称,从而使得在每次调用该类型时都能清楚地理解其用途。
密封类作为命名空间构造函数
每次都需要手动创建结果记录,这种做法既重复又容易出错。最简洁的解决方案就是使用密封类来为静态工厂方法提供一个统一的命名空间:
sealed class Res {
static Result success(T data) => (e: null, data: data);
static Result failure(E e) => (e: e, data: null);
}
需要注意的是,这里的sealed关键字并不是用于实现多态性的。这种类不能被实例化,它的存在纯粹是为了将两个相关的静态方法放在一个有意义且不可扩展的命名空间中。
使用这种方式后,代码结构会变得更加清晰、有条理:
// 在仓库模块中
Future> getUser(String id) async {
try {
final user = await _api.fetchUser(id);
return Res.success(user);
} on NetworkException catch (e) {
return Res.failure(iException.internet(message: e.message));
}
}
对于Dio框架特有的响应类型,也可以采用同样的模式:
sealed class DioResult {
static ApiResult success(T data) => (data: data, exception: null);
static ApiResult failure(E exception) => (data: null, exception: exception);
}
对于那些需要使用简化类型别名的仓库级结果类型,可以这样实现:
// GET 实际上就是 ({E? e, T? res})
typedef New = GET;
sealed class R {
static New success(T data) => (e: null, res: data);
static New failed(iException error) => (e: error, res: null);
}
每个密封类的命名空间都只负责处理一种特定的功能,并且对应于应用程序中的某一层结构。
特定领域的记录类型
对于那些不符合通用成功/失败模式的特定领域结果类型,使用记录结构也能很好地满足需求:
typedef SecurityResponse = ({bool? isSecured, String? error});
sealed class Check {
static SecurityResponse isSecured() => (isSecured: true, error: null);
static SecurityResponse isInsecured(String error) => (isSecured: false, error: error);
}
使用方法如下:
final check = Check.isSecured();
if (check.isSecured == true) {
// 继续执行后续操作
}
final check = Check.isInsecured('Certificate validation failed');
print(check.error); // 输出:Certificate validation failed
这种记录结构清晰、易于理解,而且能够自我说明功能用途。它能让你清楚地知道这个函数返回的是什么类型的数据。
基于记录的结果类型要求你必须手动检查哪些字段是非空的。编译器不会强制你处理这两种情况,也没有内置的方法可以在不手动解包数据的情况下转换结果。因此,在这种情况下,使用合适的密封型结果类型就显得十分必要了。
第2部分:构建合适的密封型结果类型
AppResult密封类
密封型结果类型的优势在于:它利用Dart的类型系统,使两种可能的结果状态在结构上变得截然不同,并提供了when()方法,强制调用者在编译时就考虑这两种情况。
import 'appfailure.dart';
sealed class AppResult {
const AppResult();
R when({
required R Function(T value) success,
required R Function(AppFailure failure) failure,
});
}
class AppSuccess extends AppResult {
const AppSuccess(this.value);
final T value;
@override
R when({
required R Function(T value) success,
required R Function(AppFailure failure) failure,
}) {
return success(value);
}
}
class AppFailureResult extends AppResult {
const AppFailureResult(this.error);
final AppFailure error;
@override
R when({
required R Function(T value) success,
required R Function(AppFailure failure) failure,
}) {
return failure(error);
}
}
让我们仔细分析一下这些设计决策的合理性。
sealed class AppResult:`sealed` 这个关键字意味着所有的子类型都必须位于同一个文件中,编译器能够了解所有可能的子类型。正是这一特性使得穷举模式匹配成为可能。T表示在操作成功时返回的数据类型。
AppSuccess:用于存储实际的结果数据。当对AppSuccess对象调用when()方法时,系统会始终执行success回调函数,并将结果值传递给该函数。
AppFailureResult:用于存储错误信息。当对AppFailureResult对象调用when()方法时,系统会始终执行failure回调函数。需要注意的是,即使这个对象不包含任何实际数据,它仍然带有T类型标注——这种设计确保了两种子类型都能与AppResult类型兼容。
when()方法:这是整个机制的核心。这两个回调函数都是必须存在的;编译器会强制要求你必须同时处理成功和失败这两种情况。你不能忽略错误路径,也不能忽略成功路径。到底是哪条路径会被执行,是由对象本身来决定的——而不是由调用代码中的if/else语句来决定的。
// 这个仓库函数会返回AppResult类型的结果
Future> login(String email, String password) async {
try {
final user = await _api.login(email, password);
return AppSuccess(user);
} on UnauthorizedException {
return AppFailureResult(AppFailure.unauthorized());
} on NetworkException {
return AppFailureResult(AppFailure.network());
} catch (e) {
return AppFailureResult(AppFailure.unknown(e.toString()));
}
}
使用when()处理结果
final result = await _repository.login(email, password);
result.when(
success: (user) => emit(AuthState.authenticated(user)),
failure: (error) => emit(AuthState.error(error.message)),
);
你也可以使用when()来返回其他类型的数据:
// 返回Widget类型的结果
final widget = result.when(
success: (user) => UserProfileCard(user: user),
failure: (error) => ErrorView(message: error.message),
);
// 返回字符串类型的结果
final message = result_when(
success: (data) => '欢迎回来,${data.name}',
failure: (error) => '发生了错误:${error.message)',
);
when()会自动推断返回类型R;无论两个回调函数返回什么类型,when()都会返回相同类型的值。如果它们返回的是Widget,那么最终得到的也是Widget;如果它们返回的是String,那么最终得到的也是String。
为什么这种设计更好
| 异常处理情况 | AppResult类型的作用 | |
|---|---|---|
| 错误信息会在调用结果中直接显示 | ❌ | ✅ |
| 编译器会强制要求必须处理成功和失败两种情况 | ❌ | ✅ |
| 在调用时必须同时处理成功和失败两种路径 | ❌ | ✅ |
| 所有代码层都能保证类型安全 | ❌ | ✅ |
| 代码结构清晰,易于理解 | ❌ | ✅ |
第3部分:扩展到Monad模式
什么使某物成为Monad?
Monad是函数式编程中的一种模式。从实际角度来看,当某种类型满足以下三个条件时,它就被视为Monad:
包装功能——你可以将一个值放入相应的上下文中。
AppSuccess(user) // 将User封装到AppResult中
转换功能(map)——你可以在不手动解包的情况下,对被包装的值应用某个函数。如果转换结果表示失败,那么这个转换操作就会被跳过,失败状态也会直接传递下去。
链式操作功能(flatMap)——你可以按顺序执行多个操作,而这些操作都会返回相同类型的封装结果,而且无需使用嵌套结构。一旦出现第一个失败,整个操作链就会立即停止执行。
如上所定义的AppResult类型满足了第一条规则,同时也符合通过when()实现的第二条规则的“精神”。但是,如果没有map和flatMap》方法,它就不符合Monad的严格定义。让我们来补上这些缺失的部分。
添加map和flatMap
sealed class AppResult {
const AppResult();
/// 转换成功的结果,失败的状态则保持不变
AppResult map(R Function(T value) transform) {
return when(
success: (value) => AppSuccess(transform(value)),
failure: (error) => AppFailureResult(error),
);
}
/// 链式执行多个操作,每个操作都会返回一个AppResult类型的结果
AppResult flatMap(AppResult Function(T value) transform) {
return when(
success: (value) => transform(value),
failure: (error) => AppFailureResult(error),
);
}
R when({
required R Function(T value) success,
required R Function(AppFailure failure) failure,
});
}
map方法会使用普通的函数来转换成功的结果。如果转换结果本身就是失败状态,map就会直接跳过这个转换步骤,让失败状态原封不动地传递下去。这种现象被称为“失败传播”——错误会自动在整个操作链中传递。
flatMap方法则会链式执行那些本身也会返回AppResult类型的操作。正是这种机制使得顺序执行多个操作成为可能——当流程中的每个步骤都有可能独立地成功或失败时,flatMap能够确保一旦出现第一个失败,整个操作链就会立即停止。
链式操作
如果没有Monad的链式操作机制,那些可能会失败的顺序执行操作看起来会像这样:
final loginResult = await login(email, password);
loginResult.when(
success: (user) async {
final profileResult = await getProfile(user.id);
profileResult WHEN(
success: (profile) async {
final settingsResult = await loadSettings(profile.settingsId);
settingsResult WHEN(
success: (settings) => emit(AppState.ready(settings)),
failure: (error) => emit(AppState.error(error)),
);
},
failure: (error) => emit(AppState.error(error)),
);
},
failure: (error) => emit(AppState.error(error)),
);
在每一个步骤中,都存在深度嵌套的、重复性的错误处理逻辑。通过使用flatMap来实现这一点:
final result = (await login(email, password))
.flatMap((user) => getProfile(user.id))
.flatMap((profile) => loadSettings(profile.settingsId))
.map((settings) => settings.theme);
result.when(
success: (theme) => emit(AppState.ready(theme)),
failure: (error) => emit(AppState.error(error)),
);
只有当前步骤成功后,后续步骤才会被执行。一旦出现第一次失败,整个处理流程就会立即终止。错误处理只在最后一步进行,而不会在每一步都发生。这就是将单子模式应用到实际代码中后所展现出的强大功能。
第4部分:使用dartz实现“Either”模式
什么是“Either”?
Either<L, R>是函数式编程中的一种类型,它表示两种可能的值之一——Left或Right。按照惯例:
-
Left—— 表示失败的情况 -
Right—— 表示成功的情况
dartz包将这种类型以及许多其他函数式编程的基本概念引入到了Dart语言中。你可以将其添加到你的项目中:
dependencies:
dartz: ^0.10.1
在我们正在开发的代码库中,Either类型是通过一个别名来使用的,这样其用途就能被清晰地表达出来:
import 'package:dartz/dartz.dart';
typedef API<T> = Either<T, iException>;
需要注意的是这里的约定:Left用来表示成功的结果T,而Right用来表示失败的原因iException。这种约定与函数式编程中的常规做法是相反的。在实际代码中,这两种约定都是存在的——关键是要保持一致性。
在实践中使用“Either”模式
创建Either类型的值:
// 成功时 —— Left用于存储数据
Either<User, iException>> result = Left(user);
// 失败时 —— Right用于存储异常信息
Either<User, iException>> result = Right(iException.internet(message: '无法建立连接'));
判断当前处于哪种情况:
if (result.isLeft()) {
final user = result.fold((user) => user, (_) => null);
}
将记录结构与“Either”模式结合起来
API类型的别名所带来的真正价值,体现在ApiRes这个工具类上。这个类能够将数据层中基于记录的结构,转换为领域层中基于Either的模式:
class ApiRes {
static Future<API<T>>> deserialize<T>>(ApiResult<T, iException>> res) async {
return (res.data != null)
? Left(res.data as T)
: Right(res.exception!);
}
static Future<API>> deserializeDynamic(
ApiResult<dynamic, iException>> res,
) async {
return (res.data != null) ? Left(res.data) : Right(res.exception!);
}
}
ApiResult 是数据层中使用的记录类型——它是一种被封装在可为空字段中的Dio响应对象。ApiRes.deserialize会将该记录转换为合适的Either类型,从而使其可以在领域层中被使用。
在实际开发中,存储库方法通常会如下所示:
Future> getUser(String id) async {
// 数据层会返回一条记录
final res = await _dataSource.fetchUser(id);
// 在层与层之间的转换点进行类型转换
return ApiRes deserialize(res);
}
各层之间的边界就是这种类型转换发生的地点。在数据层内部,你直接操作记录;在层与层之间的转换点,你会对这些记录进行类型转换;而在领域层中,你则使用Either类型来处理数据。每一层都使用最适合自己的数据类型。
折叠Either类型
dartz为Either类型提供了fold方法,这个方法的使用方式与AppResult上的when()方法类似:
final result = await repository.getUser(id);
result.fold(
(user) => emit(UserStateLoaded(user)), // 成功情况
(exception) => emit(UserState.error(exception.message)), // 失败情况
);
dartz还提供了许多用于操作Either类型的函数:
// map — 对左侧的值进行转换
final nameResult = result.map((user) => user.name);
// flatMap / bind — 将多个返回Either类型的操作串联起来
final profileResult = result.flatMap(
(user) => getProfile(user.id),
);
所有这些功能工具都已经准备好了,你可以直接使用它们,而无需自己再编写代码。
第5部分:使用Freezed库实现类型化的异常
为什么要在异常中使用Freezed库?
标准的Dart异常几乎不包含任何有用的信息:
throw Exception('出了问题');
// 在捕获异常的地方,根本不知道到底发生了什么问题、属于哪种类型的异常,也不知道是哪段代码导致了这个问题。
即使是自定义的异常类,要正确实现它们也需要编写大量的样板代码——比如定义==操作符、计算hashCode值、重写toString方法,还要确保对象的不可变性等。而使用Freezed库,这些工作都可以自动完成,此外它还提供了强大的模式匹配功能。
需要添加以下依赖包:
dependencies:
freezed_annotation: ^2.4.1
dev_dependencies:
freezed: ^2.4.5
build_runner: ^2.4.6
构建iException类型
import 'package:flutter/foundation.dart';
import 'package:freezed.annotation/freezed_annotation.dart';
part 'exception.freezed.dart';
@freezed
class iException with _$iException {
const factory iException.internet({
required String message,
int? code,
}) = InternetException;
const factory iException.mapper({
required String message,
int? code,
}) = MapperException;
const factory iException.validation({
required String message,
int? code,
}) = ValidationException;
const factory iException.unauthorized({
required String message,
int? code,
}) = UnauthorizedException;
const factory iException.unknown({
required String message,
int? code,
}) = UnknownException;
const iException._();
}
运行代码生成命令:
flutter pub run build_runner build --delete-conflicting-outputs
Freezed通过这个过程会生成以下异常类:
iException (sealed base)
├── InternetException — 网络连接失败
├── MapperException — JSON解析失败
├── ValidationException — 输入验证失败
├── UnauthorizedException — 认证失败或令牌过期
└── UnknownException — 用于处理其他未知错误
每个子类都是不可变的,其==操作符及hashCode值都是根据其字段计算得出的,同时这些子类也都实现了适当的toString方法。异常类的创建过程清晰明了:
iException.internet(message: '没有网络连接')
iException.unauthorized(message: '会话已过期', code: 401)
iExceptionvalidation(message: '电子邮件格式无效')
iException.mapper(message: '无法解析UserResponse', code: 500)
iException.unknown(message: e.toString())
当在基类中添加任何实例方法或getter时,必须使用私有的构造函数const iException._()——这样Freezed生成的子类才能调用super._()方法,而不会在基类中暴露出公共构造函数。
异常类型的模式匹配
因为maybeWhen、map和maybeMap等方法来处理各种异常情况:
exception.when(
internet: (message, code) => '没有网络连接:$message',
mapper: (message, code) => '解析错误:$message',
validation: (message, code) => '输入无效:$message',
unauthorized: (message, code) =>> '未授权——请重新登录',
unknown: (message, code) => '发生未知错误:$message',
);
所有情况都必须被考虑到;编译器会拒绝那些处理不完整的异常匹配逻辑。你不能不小心只处理了某些类型的异常,而忽略了其他类型。
对于只需要关注特定类型的情况,可以使用以下代码:
exception.maybeWhen(
unauthorized: (message, code) =>> _redirectToLogin(),
orElse: () =>> _showGenericError(exception),
);
更简洁的基类getter模式
在基类UnimplementedError错误:
const iException._();
String get displayMessage => when(
internet: (message, _) =>> message,
mapper: (message, _) =>> message,
validation: (message, _) =>> message,
unauthorized: (message, _) =>> message,
unknown: (message, _) =>> message,
);
现在,任何包含iException的代码——无论它是哪种子类型——都可以安全地调用.displayMessage方法:
在ViewModel或BLoC中,无需专门为处理消息而进行模式匹配,
可以直接调用:emit(ErrorState(message: exception.displayMessage));
这种方法比那种在运行时抛出UnimplementedError的基类获取器要简洁得多。
第6部分:整体整合
完整架构
以下是这四种模式在Flutter应用程序中是如何相互连接的:
数据层
Dio/HTTP请求返回原始响应
└── 被封装在ApiResult< T, iException >类中(用于存储结果数据)
│
▼
仓库层
ApiRes.deserialize()方法将原始数据转换为Either< T, iException >类型
└── 最终返回API结果,即Either< T, iException >
│
▼
领域/用例层
AppResult< T>是标准的返回类型
└── 包含AppSuccess和AppFailureResult两种状态
│
▼
呈现层
result.when()方法处理成功与失败两种情况
└── exception when()方法处理所有错误类型
每一层都使用适合自身职责的结果类型。数据转换发生在各层之间的边界处。呈现层始终只处理AppResult< T >类型,它不需要了解Either或记录这类数据结构。
仓库层
class AuthRepository {
final AuthDataSource _dataSource;
AuthRepository(this._dataSource);
Future> login(String email, String password) async {
// 数据源返回记录数据
final res = await _dataSource.login(email, password);
// 在数据层与领域层之间进行转换
final either = await ApiRes.deserialize(res);
// 再将Either类型转换为AppResult类型,以便领域层使用
return either.fold(
(user) => AppSuccess(user),
(exception) => AppFailureResult(exception),
);
}
Future>> getUsers() async {
final res = await _dataSource.fetchUsers();
final either = await ApiRes.deserialize>(res);
return either.fold(
(users) => AppSuccess(users),
(exception) => AppFailureResult EXCEPTION),
);
}
}
领域层
class LoginUseCase {
final AuthRepository _repository;
LoginUseCase(this._repository);
Future> execute(String email, String password) async {
if (email.isEmpty || password.isEmpty) {
return AppFailureResult(
iException.validation(message: '必须填写电子邮件和密码'),
);
}
return _repository.login(email, password);
}
}
这种使用场景添加了自己的验证机制——在数据甚至还未到达存储层之前,就会返回一个ValidationException异常。无论故障起源于何处,所有错误都会通过相同的AppResult类型进行处理。
表现层
class AuthViewModel extends ChangeNotifier {
final LoginUseCase _loginUseCase;
AuthViewModel(this._loginUseCase);
AuthState _state = const AuthState.idle();
AuthState get state => _state;
Future login(String email, String password) async {
_state = const AuthState.loading();
notifyListeners();
final result = await _loginUseCase.execute(email, password);
result.when(
success: (user) {
_state = AuthState.authenticated(user);
},
failure: (exception) {
// 根据异常类型进行相应的处理
final message = exception_when(
internet: (msg, _) => '没有网络连接。请检查您的网络连接。",
unauthorized: (msg, _) => '您的会话已过期。请重新登录。',
validation: (msg, _) =>> msg,
mapper: (msg, _) => '发生了错误。请重试。',
unknown: (msg, _) => '出现了未知错误。',
);
_state = AuthState.error(message);
},
);
notifyListeners();
}
}
这里采用了两层详尽的模式匹配机制:一层用于处理结果,另一层用于处理异常类型。每种可能的失败情况都会对应一条用户友好的提示信息。编译器能够确保没有任何错误会被忽略。
同时,还利用了第三部分中介绍的 Monad链式编程模型来实现多步骤的处理流程:
Future loadDashboard(String userId) async {
_state = const DashboardState.loading();
notifyListeners();
final result = (await _userRepo.getUser(userId))
.flatMap((user) => _profileRepo.getProfile(user.profileId))
.flatMap((profile) => _settingsRepo.loadSettings(profile.settingsId))
.map((settings) =>> DashboardData(settings: settings));
result.when(
success: (data) => _state = DashboardState.loaded(data),
failure: (exception) => _state = DashboardState.error(
exception.displayMessage,
),
);
notifyListeners();
}
这三步异步操作中的每一步都可能失败,但整个处理流程被组织成了一条清晰的链式结构,最终只使用了一个错误处理器来处理所有错误。这才是真正符合生产环境要求的错误处理方式。
结论
错误处理是每个代码库都会涉及的内容,但很少有代码库能将其处理得当。在Dart中,抛出和捕获异常这种处理方式对于小型项目来说确实很方便,但在大规模应用中却会变成一种负担。故障现象会变得难以察觉,不同层次之间的类型信息也会丢失,而且当出现问题时,编译器也无法提供任何帮助。
本文介绍的各种模式彻底改变了这一现状。
这些设计模式能够为你提供结构简洁、无需编写大量冗余代码的结果容器,非常适合用于处理特定层次的结构化数据或针对特定领域的响应信息。采用“密封结果类型”这一机制后,编译器会强制执行相关的错误处理规则;这两种方法都是必不可少的,因为任何错误都不能被默默地忽略掉。而“单子模式”则使得顺序操作可以被清晰地串联起来,并且错误能够自动在整个操作链中传播。使用dartz>框架,你还能获得一整套函数式编程工具,同时你的数据层与业务逻辑层之间也会存在明确的边界。此外,“冻结异常”机制能为错误状态提供结构化处理方式,确保其不可变性,并支持全面的模式匹配,这样一来,每种错误类型都能被明确地处理掉,没有任何错误会被遗漏。
一旦你理解了这些设计模式所要解决的问题,就会发现它们其实并不复杂。而它们所要解决的问题——也就是那些看不见、无法被强制执行、存在类型安全风险的错误处理机制——正是Flutter应用程序中产生故障的最常见原因之一。
下一步就是将这些设计模式应用到实际项目中去。使用它们肯定能够彻底改善你整个代码库中的错误处理机制及相关流程。