Result
通常希望函数成功返回一些数据,或者如果失败则返回错误。我们通常使用throwing
函数对此建模,因为如果函数调用成功,我们将获得数据,但是如果抛出错误,则将运行catch
代码块,因此我们可以独立处理这两个函数。但是,如果函数调用没有立即返回怎么办?
我们之前使用URLSession
查看了网络代码。现在来看另一个示例,将其添加到默认的SwiftUI模板代码中:
Text("Hello, World!")
.onAppear {
let url = URL(string: "https://www.apple.com")!
URLSession.shared.dataTask(with: url) { data, response, error in
if data != nil {
print("We got data!")
} else if let error = error {
print(error.localizedDescription)
}
}.resume()
}
加载文本视图后,网络请求将立即开始,从 apple.com 提取一些数据,并根据网络请求是否起作用打印两个消息之一。
如果您还记得的话,我说完成闭包将把data
或error
设置为一个值——不能两者皆有,也不能两者都没有,因为这两种情况不会一起出现。但是,由于URLSession
对我们没有强制执行此约束,因此我们需要编写代码来处理不可能的情况,只是要确保覆盖所有情况。
Swift为解决这种混乱提供了解决方案,它是一种称为Result
的特殊数据类型。这为我们提供了所需的行为,同时还可以与非阻塞函数配合使用,这些函数是异步执行工作的,因此它们不会阻塞主代码的运行。另外,它还使我们可以返回特定类型的错误,从而更容易知道出了什么问题。
一开始可能感觉语法有点奇怪,这就是为什么我要缓慢地给您热身的原因——这个东西确实很有用,但是如果您深入一探,可能会感觉就像倒退了一步。
我们要做的是为上述网络代码创建一个包装器,以便它使用 Swift 的Result
类型,这意味着您可以清楚地看到前后。
首先,我们需要定义可以引发哪些错误。您可以定义任意多个,但在这里我们将说 URL 错误,请求失败或发生未知错误。将此枚举放在ContentView
结构体之外:
enum NetworkError: Error {
case badURL, requestFailed, unknown
}
接下来,我们将编写一个返回Result
的方法。请记住,Result
是为了表示某种成功或失败而设计的,在这种情况下,我们要说的是,成功案例将包含从网络返回的任何内容的字符串,而错误将是某种NetworkError
。
我们将四次编写相同的方法,但是会增加复杂性,因此您可以了解到底该如何使用。首先,我们将立即发送一个badURL
错误,这意味着将此方法添加到ContentView
中:
func fetchData(from urlString: String) -> Result<String, NetworkError> {
.failure(.badURL)
}
如您所见,该方法的返回类型为Result <String,NetworkError>
,表示成功时为字符串,失败时为NetworkError
值。尽管非常快,但这仍然是一个阻塞函数调用。
我们真正想要的是一个非阻塞调用,这意味着我们无法将Result
作为返回值发送回去。取而代之的是,我们需要使我们的方法接受两个参数:一个用于要获取的URL,另一个是将用值调用的完成闭包。这意味着该方法本身不返回任何内容。它的数据通过完成关闭传递回去,将来会在某个时候调用。
同样,我们将使此返回.badURL
错误,以使事情变得简单。代码如下:
func fetchData(from urlString: String, completion: (Result<String, NetworkError>) -> Void) {
completion(.failure(.badURL))
}
现在,我们有一个完成闭包的原因是我们现在可以使该方法成为非阻塞的:我们可以开始一些异步工作,使方法返回,以便其余代码可以继续,然后在稍后的任何时候调用完成闭包。
这里有一个很小的复杂性,尽管我之前已经简短地提到了它,但它变得很重要。当我们将闭包传递给函数时,Swift需要知道是立即使用它还是以后使用它。如果立即使用默认值——那么Swift很乐意运行闭包。但是,如果稍后使用它,则可能创建的闭包已被销毁并且不再存在于内存中,在这种情况下,闭包也将被销毁并且无法再运行。
为了解决这个问题,Swift让我们将闭包参数标记为@escaping
,这意味着:
对于我们的方法,我们将运行一些异步工作,然后在完成后调用闭包。这可能立即发生,也可能需要几分钟。我们不在乎。关键是方法返回后,闭包仍需要保留,这意味着我们需要将其标记为@escaping
。如果您担心忘记这一点,没有必要:Swift始终会拒绝构建代码,除非您添加@escaping
属性。
这是我们函数的第三个版本,它使用@escaping
作为闭包,因此我们可以异步调用它:
func fetchData(from urlString: String, completion: @escaping (Result<String, NetworkError>) -> Void) {
DispatchQueue.main.async {
completion(.failure(.badURL))
}
}
请记住,可以在将来的任何时候调用完成闭包,并且仍然可以正常使用。
现在,对于该方法的第四个版本,我们将把Result
代码与之前的URLSession
代码混合。这将具有完全相同的函数签名——接受字符串和闭包,但不返回任何内容——但现在我们将以不同的方式调用完成闭包:
completion(.failure(.badURL))
。completion(.success(stringData))
。completion(.failure(.requestFailed))
。completion(.failure(.unknown))
。唯一的新事物是如何将Data
实例转换为字符串。如果您还记得的话,以前使用过 let data = Data(someString.utf8)
,当从Data
转换为String
时,代码有些相似:
let stringData = String(decoding: data, as: UTF8.self)
好的,现在是我们第四遍方法的时候了:
func fetchData(from urlString: String, completion: @escaping (Result<String, NetworkError>) -> Void) {
// 检查URL是否正常,否则返回失败
guard let url = URL(string: urlString) else {
completion(.failure(.badURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
// 任务已完成–将工作移动到主线程
DispatchQueue.main.async {
if let data = data {
// 成功:将数据转换为字符串并返回
let stringData = String(decoding: data, as: UTF8.self)
completion(.success(stringData))
} else if error != nil {
// 任何形式的网络故障
completion(.failure(.requestFailed))
} else {
// 这个应该不可能发生,但我们在这里写一下
completion(.failure(.unknown))
}
}
}.resume()
}
我知道它花了很多时间,但是我想一步一步地解释它,因为有很多需要接受的东西。它为我们提供了更加简洁的API,因为我们现在可以始终确保我们可以得到一个字符串或错误——无法同时获得它们或两者都不是,因为那不是Result
的工作原理。更好的是,如果确实收到错误,则它一定是NetworkError
中指定的情况之一,这使错误处理变得容易得多。
到目前为止,我们所做的只是编写使用Result
的函数;我们还没有编写任何能处理返回结果的文件。请记住,无论发生什么情况,结果始终包含两条信息:结果的类型(成功或失败)以及其中的某些内容。对我们来说,可以是字符串,也可以是NetworkError
。\
在幕后,Result
实际上是一个具有关联值的枚举,Swift具有非常特殊的语法来处理这些值:我们可以打开Result
,并编写诸如case .success(let str)
之类的情况表示“如果这是成功后,将字符串里面的内容赋值一个名为str
的新常量。
看到所有这些都比较容易,因此让我们将新方法附加到onAppear
闭包中,并处理所有可能的情况:
Text("Hello, World!")
.onAppear {
self.fetchData(from: "https://www.apple.com") { result in
switch result {
case .success(let str):
print(str)
case .failure(let error):
switch error {
case .badURL:
print("Bad URL")
case .requestFailed:
print("Network problems")
case .unknown:
print("Unknown error")
}
}
}
}
希望现在你能看到好处:我们不仅消除了检查返回的内容的不确定性,还完全消除了可选值。甚至连错误处理的默认情况都不需要了,因为所有可能的NetworkError
情况都被覆盖了。