SE-0235 在标准库中引入了一个 Result
类型,使我们能够更简单、更清晰地处理复杂代码中的错误,比如异步 API。这是人们在 Swift 早期就开始要求的东西,所以很高兴看到它终于到来!
Swift 的 Result
类型被实现为一个枚举,它有两种情况:success
和 failure
。两者都是使用泛型实现的,因此它们可以有您选择的关联值,但 failure
必须符合 Swift 的 Error
类型。
为了演示 Result
,我们可以编写一个网络请求函数来计算有多少未读消息在等待用户。在这个例子代码中,我们将只有一个可能的错误,那就是请求的 URL 字符串不是一个有效的 URL:
1
2
3
enum NetworkError: Error {
case badURL
}
这个函数将接受一个 URL 字符串作为它的第一个参数,并接受一个 completion 闭包作为它的第二个参数。该 completion 闭包本身接受一个 Result
,其中 success
将存储一个整数,而 failure
案例将是某种 NetworkError
。我们实际上并不打算在这里连接到服务器,但是使用一个 completion 闭包至少可以让我们模拟异步代码。
代码如下:
1
2
3
4
5
6
7
8
9
10
func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void) {
guard let url = URL(string: urlString) else {
completionHandler(.failure(.badURL))
return
}
// complicated networking code here
print("Fetching \(url.absoluteString)...")
completionHandler(.success(5))
}
要使用该代码,我们需要检查我们的 Result
中的值,看看我们的调用成功还是失败,如下所示:
1
2
3
4
5
6
7
8
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
switch result {
case .success(let count):
print("\(count) unread messages.")
case .failure(let error):
print(error.localizedDescription)
}
}
即使在这个简单的场景中,Result
也给了我们两个好处。首先,我们返回的错误现在是强类型的:它一定是某种 NetworkError
。Swift 的常规抛出函数是不检查类型的,因此可以抛出任何类型的错误。因此,如果您添加了一个 switch
语句来查看他们的情况,您需要添加 default
情况,即使这种情况是不可能的。使用 Result
的强类型错误,我们可以通过列出错误枚举的所有情况来创建详尽的 switch
语句。
其次,现在很清楚,我们要么返回成功的数据要么返回一个错误,它们两个中有且只有一个一定会返回。如果我们使用传统的 Objective-C 方法重写 fetchUnreadCount1()
来完成 completion 闭包,你可以看到第二个好处的重要性:
1
2
3
4
5
6
7
8
9
func fetchUnreadCount2(from urlString: String, completionHandler: @escaping (Int?, NetworkError?) -> Void) {
guard let url = URL(string: urlString) else {
completionHandler(nil, .badURL)
return
}
print("Fetching \(url.absoluteString)...")
completionHandler(5, nil)
}
这里,completion 闭包将同时接收一个整数和一个错误,尽管它们中的任何一个都可能是 nil。Objective-C 之所以使用这种方法,是因为它没有能力用关联的值来表示枚举,所以别无选择,只能将两者都发送回去,让用户自己去弄清楚。
然而,这种方法意味着我们已经从两种可能的状态变成了四种:一个没有错误的整数,一个没有整数的错误,一个错误和一个整数,没有整数和没有错误。最后两种状态应该是不可能的,但在 Swift 引入 Result
之前,没有简单的方法来表达这一点。
这种情况经常发生。URLSession
中的 dataTask()
方法使用相同的解决方案,例如:它用 (Data?, URLResponse?, Error?)
。这可能会给我们提供一些数据、一个响应和一个错误,或者三者的任何组合 — Swift Evolution 的提议称这种情况“尴尬不堪”。
可以将 Result
看作一个超级强大的 Optional
,Optional
封装了一个成功的值,但也可以封装第二个表示没有值的情况。然而,对于 Result
,第二种情况还可以传递了额外的数据,因为它告诉我们哪里出了问题,而不仅仅是 nil
。
为何不使用 throws
?
当你第一次看到 Result
时,你常常会想知道它为什么有用,尤其是自从 Swift 2.0 以来,它已经有了一个非常好的 throws
关键字来处理错误。
你可以通过让 completion 闭包接受另一个函数来实现几乎相同的功能,该函数会抛出或返回有问题的数据,如下所示:
1
2
3
4
5
6
7
8
9
func fetchUnreadCount3(from urlString: String, completionHandler: @escaping (() throws -> Int) -> Void) {
guard let url = URL(string: urlString) else {
completionHandler { throw NetworkError.badURL }
return
}
print("Fetching \(url.absoluteString)...")
completionHandler { return 5 }
}
然后,您可以使用一个接受要运行的函数的 completion 闭包调用 fetchUnreadCount3()
,如下所示:
1
2
3
4
5
6
7
8
fetchUnreadCount3(from: "https://www.hackingwithswift.com") { resultFunction in
do {
let count = try resultFunction()
print("\(count) unread messages.")
} catch {
print(error.localizedDescription)
}
}
这也能解决问题,但读起来要复杂得多。更糟的是,我们实际上并不知道调用 result()
函数是做什么的,所以如果它不仅仅返回一个值或抛出一个值,那么就有可能导致它自己的问题。
即使使用更简单的代码,使用 throws
也常常迫使我们立即处理错误,而不是将错误存储起来供以后处理。有了 Result
,这个问题就消失了,错误被保存在一个值中,我们可以在准备好时读取这个值。
处理 Result
我们已经了解了 switch
语句如何让我们以一种干净的方式评估 Result
的 success
和 failure
案例,但是在开始使用它之前,还有五件事您应该知道。
首先,Result
有一个 get()
方法,如果存在则返回成功值,否则抛出错误。这允许您将 Result
转换为一个常规抛出调用,如下所示:
1
2
3
4
5
6
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
if let count = try? result.get() {
print("\(count) unread messages.")
}
}
其次,如果您愿意,可以使用常规的 if
语句来读取枚举的情况,尽管有些人觉得语法有点奇怪。例如:
1
2
3
4
5
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
if case .success(let count) = result {
print("\(count) unread messages.")
}
}
第三,Result
有一个接受可能会抛出错误的闭包的初始化器:如果闭包成功返回一个值,该值用于 success
情况,否则抛出的错误将被放入 failure
情况。
例如:
1
let result = Result { try String(contentsOfFile: someFile) }
第四,您还可以使用一般的 Error
协议,而不是使用您创建的特定错误枚举。事实上,Swift Evolution 的提议说:“预计 Result
的大多数用法都将使用 Swift.Error
作为 Error
类型参数。”
因此,可以使用 Result<Int, Error>
而不是 Result<Int, NetworkError>
。虽然这意味着您失去了类型抛出的安全性,但是您获得了抛出各种不同错误枚举的能力 —— 您更喜欢哪种错误枚举实际上取决于您的编码风格。
最后,如果你已经在你的项目中有了一个自定义的 Result
类型(任何你自己定义的或者从 GitHub 上的自定义 Result
类型导入的),那么它们将自动代替 Swift 自己的 Result
类型。这将允许您在不破坏代码的情况下升级到 Swift 5.0,但理想情况下,随着时间的推移,您将迁移到 Swift 自己的 Result
类型,以避免与其他项目不兼容。
转换 Result
Result
有另外四个可能被证明有用的方法:map()
、flatMap()
、 mapError()
和 flatMapError()
。这几个方法都能够以某种方式转换成功或错误,前两种方法和 Optional
上的同名方法行为类似。
map()
方法查看 Result
内部,并使用指定的闭包将成功值转换为另一种类型的值。但是,如果它发现失败,它只直接使用它,而忽略您的转换。
为了演示这一点,我们将编写一些代码,生成 0 到最大值之间的随机数,然后计算该数的因数。如果用户请求一个小于零的随机数,或者这个随机数恰好是素数,即它没有其他因数,除了它自己和 1,我们会认为这些都是失败情况。
我们可以从编写代码开始,对两种可能的失败案例进行建模:用户试图生成一个小于 0 的随机数和生成的随机数是素数:
1
2
3
4
enum FactorError: Error {
case belowMinimum
case isPrime
}
接下来,我们将编写一个函数,它接受一个最大值,并返回一个随机数或一个错误:
1
2
3
4
5
6
7
8
9
func generateRandomNumber(maximum: Int) -> Result<Int, FactorError> {
if maximum < 0 {
// creating a range below 0 will crash, so refuse
return .failure(.belowMinimum)
} else {
let number = Int.random(in: 0...maximum)
return .success(number)
}
}
当它被调用时,我们返回的 Result
要么是一个整数,要么是一个错误,所以我们可以使用 map()
来转换它:
1
2
let result1 = generateRandomNumber(maximum: 11)
let stringNumber = result1.map { "The random number is: \($0)." }
当我们传入一个有效的最大值时,result1
将是一个成功的随机数。因此,使用 map()
将获取这个随机数,并将其与字符串插值一起使用,然后返回另一个 Result
类型,这次的类型是 Result< string, FactorError>
。
但是,如果我们使用了 generateRandomNumber(maximum: -11)
,那么 result1
将被设置为 FactorError.belowMinimum
的失败情况。因此,使用 map()
仍然会返回 Result<String, FactorError>
,但是它会有相同的失败情况和相同的 FactorError.belowMinimum
错误。
既然您已经了解了 map()
如何让我们将成功类型转换为另一种类型,那么让我们继续,我们有一个随机数,因此下一步是计算它的因数。为此,我们将编写另一个函数,它接受一个数字并计算其因数。如果它发现数字是素数,它将返回一个带有 isPrime
错误的失败 Result
,否则它将返回因数的数量。
这是代码:
1
2
3
4
5
6
7
8
9
10
func calculateFactors(for number: Int) -> Result<Int, FactorError> {
let factors = (1...number).filter { number % $0 == 0 }
if factors.count == 2 {
return .failure(.isPrime)
} else {
return .success(factors.count)
}
}
如果我们想使用 map()
来转换 generateRandomNumber()
生成随机数后再 calculateFactors()
的输出,它应该是这样的:
1
2
3
let result2 = generateRandomNumber(maximum: 10)
let mapResult = result2.map { calculateFactors(for: $0) }
然而,这使得 mapResult
成为一个相当难看的类型:Result<Result<Int, FactorError>, FactorError>
。它是另一个 Result
内部的一个 Result
。
就像可选值一样,现在是 flatMap()
方法起作用的时候了。如果你的转换闭包返回一个 Result
,flatMap()
将直接返回新的 Result
,而不是包装在另一个 Result
内:
1
let flatMapResult = result2.flatMap { calculateFactors(for: $0) }
因此,其中 mapResult
是一个 Result<Result<Int, FactorError>, FactorError>
,flatMapResult
被展平成 Result<Int, FactorError>
– 第一个原始成功值(一个随机数)被转换成一个新的成功值(因数的数量)。就像 map()
一样,如果其中一个 Result
失败,那么 flatMapResult
也将失败。
至于 mapError()
和 flatMapError()
,除了转换 error 值而不是 success 值外,它们执行类似的操作。