Swift的一个主要关注点是编译时的安全性——使我们作为开发人员能够轻松地专注于编写更可预测、更不易发生运行时错误的代码。 然而,有时候事情会因为各种原因而失败——所以这周,让我们看看我们可以如何恰当地处理这样的失败,以及我们可以使用什么工具来这样做。
几周前,我们在“处理Swift中非可选的可选选项”中查看了如何处理非可选的可选选项。在那篇文章中,我介绍了将preconditionFailure()与guard结合使用的情况,而不是强制展开,并介绍了微框架需求,该需求为这样做提供了一个方便的API。
自从那篇文章发表后,很多人都在问preconditionFailure()和assert()之间有什么区别,以及它与Swift的抛出能力有什么关系。因此,在这篇文章中,让我们仔细看看所有这些语言特性,以及何时使用它们。
Let’s start with a list
以下是所有(据我所知)你可以处理Swift错误的方法:
Return nil or an error enum case. 最简单的错误处理形式是简单地从遇到错误的函数返回nil(或者如果您使用Result enum作为返回类型,则返回.error情况)。虽然这在许多情况下确实很有用,但过度使用它来处理所有错误会很快导致api使用起来很麻烦,而且还可能隐藏错误逻辑。
Throwing an error (using throw MyError) 它要求调用者使用do, try, catch模式来处理潜在的错误。或者,可以使用try?在呼叫点。
Using assert() and assertionFailure() to verify that a certain condition is true. 默认情况下,这会在调试构建中导致致命错误,而在发布构建中会被忽略。因此,不能保证如果断言被触发,执行会停止,这有点像一个严重的运行时警告。
Using precondition() and preconditionFailure() instead of asserts. 关键的区别是这些总是被执行的,即使是在发布版本中。这意味着,如果不满足条件,就保证不会继续执行。
Calling fatalError()你可能在xcode生成的init(coder:)实现中看到过 当子类化符合nscoding的系统类时,比如UIViewController。直接调用这个函数会终止进程。
Calling exit() which exists your process with a code。这在命令行工具和脚本中非常有用,当你想退出全局作用域时(例如在main.swift中)。
Recoverable vs non-recoverable
在选择正确的失败方式时,要考虑的关键问题是确定所发生的错误是否可以恢复。
例如,假设我们正在调用服务器,并收到了一个错误响应。这是必然会发生的事情,无论我们的程序员有多么优秀,我们的服务器基础设施有多么健壮。因此,将这些类型的错误视为致命的和不可恢复的通常是一个错误。 相反,我们想要的是恢复并可能向用户显示某种形式的错误屏幕。
那么,在这种情况下,如何选择一种合适的失败方式呢? 如果我们看一下上面的列表,我们可以把它分成可恢复技术和不可恢复技术,就像这样:
Recoverable
Returning nil or an error enum case
Throwing an error
Non-recoverable
Using assert()
Using precondition()
Calling fatalError()
Calling exit()
在这种情况下,因为我们正在处理一个异步任务,返回nil或一个错误枚举可能是最好的选择,像这样:
class DataLoader {
enum Result {
case success(Data)
case failure(Error?)
}
func loadData(from url: URL,
completionHandler: @escaping (Result) -> Void) {
let task = urlSession.dataTask(with: url) { data, _, error in
guard let data = data else {
completionHandler(.failure(error))
return
}
completionHandler(.success(data))
}
task.resume()
}
}
对于同步API,抛出是一个很好的选择——因为它“强迫”我们的API用户以适当的方式处理错误:
class StringFormatter {
enum Error: Swift.Error {
case emptyString
}
func format(_ string: String) throws -> String {
guard !string.isEmpty else {
throw Error.emptyString
}
return string.replacingOccurences(of: "\n", with: " ")
}
}
然而,有时错误是不可恢复的。 例如,假设我们需要在应用程序启动时加载一个配置文件。 如果那个配置文件丢失了,它会把我们的应用置于未定义状态 -所以在这种情况下崩溃比继续执行程序要好。 因此,使用一种更强的、不可恢复的失败方式更合适。
在这种情况下,我们使用preconditionFailure()来停止执行,以防配置文件丢失:
guard let config = FileLoader().loadFile(named: "Config.json") else {
preconditionFailure("Failed to load config file")
}
Programmer errors vs execution errors
另一个重要的区别是错误是由错误的逻辑或错误的配置引起的,还是应该将错误视为应用程序流的合法部分。 基本上是程序员造成的错误还是外部因素造成的。
在防止程序员错误时,您几乎总是希望使用不可恢复技术。这样,你就不必在应用程序的所有特殊环境下编写代码,而一套良好的测试将确保尽早发现这些类型的错误。
例如,假设我们正在构建一个视图,在使用它之前需要将视图模型绑定到它。 视图模型在我们的代码中是可选的,但我们不想每次使用它时都必须unwrap它。
然而,如果视图模型不知为何丢失了,我们并不一定希望在生产环境中使应用程序崩溃,在调试中得到一个关于它的错误就足够了。这是一个使用assert的例子:
class DetailView: UIView {
struct ViewModel {
var title: String
var subtitle: String
var action: String
}
var viewModel: ViewModel?
override func didMoveToSuperview() {
super.didMoveToSuperview()
guard let viewModel = viewModel else {
assertionFailure("No view model assigned to DetailView")
return
}
titleLabel.text = viewModel.title
subtitleLabel.text = viewModel.subtitle
actionButton.setTitle(viewModel.action, for: .normal)
}
}
注意,我们必须在上面的guard语句中返回,因为assertionFailure()将在发布构建中静默失败。
Conclusion
我希望这篇文章有助于澄清Swift中各种错误处理技术之间的区别。我的建议是,不仅要坚持一种技巧,还要根据情况选择最合适的一种。一般来说,我还建议,如果可能的话,总是尝试从错误中恢复,除非错误应该被视为致命的,否则不要破坏用户体验。
另外,请记住print(error)不是错误处理😉