[SwiftUI 100 天] Hot Prospects - 理解 Swift 的 Result 类型
2020/6/10 23:26:40
本文主要是介绍[SwiftUI 100 天] Hot Prospects - 理解 Swift 的 Result 类型,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
理解 Swift 的 Result 类型
让一个函数在执行成功时返回某些数据,执行失败时返回某个错误是很常见的做法。我们通常会利用抛出错误的函数来实现这个要求,一旦函数抛出错误运行 catch
块,这样就独立地处理成功和失败的逻辑。但是假如函数并不是立即返回的呢?
我们可以回顾一下之前使用过的 URLSession
的网络代码,然后在一个新的默认工程里看看下面这样一个例子:
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
的某一种。
我们要逐渐加大难度,把同一个方法的写法升级四次。系好安全带,你会看到东西是怎么建起来的。先从最简单的版本开始,我们直接返回一个 URL 错误的版本,像下面这样:
func fetchData(from urlString: String) -> Result<String, NetworkError> { .failure(.badURL) } 复制代码
如你所见,方法的返回类型是 Result<String, NetworkError>
,也就说,要么是一个代表成功的字符串,要么是代表失败的某个 NetworkError
。注意,这个时候函数还是阻塞式的调用,一个非常快的调用。
当我们实际上要的是一个非阻塞式的函数,也就是说,我们不能返回一个 Result
。取而代之的是,我们需要让我们的方法接收两个参数:一个用于 URL 请求,另一个是带一个执行参数的完成闭包。这意味着函数本身不返回任何东西,它的数据会被返回给完成闭包,这个闭包是在未来某个节点被调用。
再一次,为了让事情简化,我们还是直接使用 URL 错误的失败作为默认的实现:
func fetchData(from urlString: String, completion: (Result<String, NetworkError>) -> Void) { completion(.failure(.badURL)) } 复制代码
我们使用完成闭包的目的是让方法变成非阻塞式的:在方法里面,我们可以启动一些异步的工作,让方法直接返回,以便后面的代码能够继续运行,然后在未来某个时间调用完成闭包。
这里面有一个难点,我之前简要提过,现在变得很重要了。当我们把一个闭包传给一个函数时,Swift 需要知道这个闭包是被立刻使用还是可能稍后才被使用。如果它是被立即使用的 —— 也就是默认的情况 —— Swift 很欣然接受代码,然后运行闭包。但如果它是稍后才使用的,那么很有可能创建闭包的东西在闭包被调用时已经被销毁掉,不再存在于内存中,这个时候闭包也会被销毁,不被执行。
为了处理这种情况,Swift 允许我们给闭包参数标记 @escaping
(逃逸闭包),它的意思是“这个闭包可能会脱离当前方法的运行周期被使用,所以请在内存中保留它,直到我们把事情做完。”
以我们的方法为例,我们将先执行一个异步的工作,然后调用在该工作做完时调用你闭包。这个调用动作可能立即发生,也可能需要几分钟。但我们不关心这一点,关键是闭包在方法返回之后还需要保留,因此我们必须把它标记为@escaping
。你可能会担心自己遗漏这一点,大可不必担心:如果你不加 @escaping
属性的话 Swift 实际上回拒绝编译。
下面是函数的第三个版本,使用了 @escaping
的闭包,以便我们可以异步调用:
func fetchData(from urlString: String, completion: @escaping (Result<String, NetworkError>) -> Void) { DispatchQueue.main.async { completion(.failure(.badURL)) } } 复制代码
记住,完成闭包是在未来某个时点被调用的。
最后是第四个版本:我们要讲 URLSession
的 code 合入之前的 Result
。这个版本的函数签名不变 —— 仍是接收一个字符串和一个闭包,不返回任何东西 —— 但这次我们调用完成闭包的方式不同:
- 如果 URL 非法,我们调用
completion(.failure(.badURL))
。 - 如果我们从请求的返回中得到合法的数据,则将其转换成字符串并调用
completion(.success(stringData))
。 - 如果我们从请求得到错误,则调用
completion(.failure(.requestFailed))
。 - 如果既没有得到数据,也没有得到错误,则调用
completion(.failure(.unknown))
。
这里头唯一的新知识点是将 Data
实例转换成字符串。回忆一下,你知道如何从字符串构建 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) { // check the URL is OK, otherwise return with a failure guard let url = URL(string: urlString) else { completion(.failure(.badURL)) return } URLSession.shared.dataTask(with: url) { data, response, error in // the task has completed – push our work back to the main thread DispatchQueue.main.async { if let data = data { // success: convert the data to a string and send it back let stringData = String(decoding: data, as: UTF8.self) completion(.success(stringData)) } else if error != nil { // any sort of network failure completion(.failure(.requestFailed)) } else { // this ought not to be possible, yet here we are completion(.failure(.unknown)) } } }.resume() } 复制代码
讲完四个版本的函数费了不少篇幅,之所以一步一步解释的原因在于需要理解消化的内容着实不少。 最后的代码实现了一个更清爽的 API,借助它我们可以确保要么得到字符串,要么得到某个错误 —— 不可能同时得到两者或两者都得不到,这正是 Result
的特点。更棒的是,我们得到错误的话,必定是 NetworkError
的某一条 case,这使得错误处理更加容易。
目前为止我们实现了使用 Result
的函数,但还没有编写处理 Result
的函数。无论何种情况,Result
总是携带两部分的信息:结果的类型(成功或者失败),以及内部包含的东西。对于我们而言,这东西就是字符串或者某个 NetworkError
。
在幕后,Result
实际上是一个有关联值的枚举,Swift 对此提供了特别的语法:我们可以对 Result
使用 switch
,编写像 .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") } } } } 复制代码
希望你能发现这么做的益处:我们不仅消除了对于返回的数据做检查的不确定因素,也完全消除了可选性。对于错误处理,甚至不再需要 default
的 case,因为 NetworkError
的所有 case 都会被覆盖到。
我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~
这篇关于[SwiftUI 100 天] Hot Prospects - 理解 Swift 的 Result 类型的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值