前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >了解 Swift 的 Result 类型

了解 Swift 的 Result 类型

作者头像
韦弦zhy
发布2020-09-14 11:17:01
2.6K0
发布2020-09-14 11:17:01
举报
文章被收录于专栏:韦弦的偶尔分享

Result

通常希望函数成功返回一些数据,或者如果失败则返回错误。我们通常使用throwing函数对此建模,因为如果函数调用成功,我们将获得数据,但是如果抛出错误,则将运行catch代码块,因此我们可以独立处理这两个函数。但是,如果函数调用没有立即返回怎么办?

我们之前使用URLSession查看了网络代码。现在来看另一个示例,将其添加到默认的SwiftUI模板代码中:

代码语言:javascript
复制
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 提取一些数据,并根据网络请求是否起作用打印两个消息之一。

如果您还记得的话,我说完成闭包将把dataerror设置为一个值——不能两者皆有,也不能两者都没有,因为这两种情况不会一起出现。但是,由于URLSession对我们没有强制执行此约束,因此我们需要编写代码来处理不可能的情况,只是要确保覆盖所有情况。

Swift为解决这种混乱提供了解决方案,它是一种称为Result的特殊数据类型。这为我们提供了所需的行为,同时还可以与非阻塞函数配合使用,这些函数是异步执行工作的,因此它们不会阻塞主代码的运行。另外,它还使我们可以返回特定类型的错误,从而更容易知道出了什么问题。

一开始可能感觉语法有点奇怪,这就是为什么我要缓慢地给您热身的原因——这个东西确实很有用,但是如果您深入一探,可能会感觉就像倒退了一步。

我们要做的是为上述网络代码创建一个包装器,以便它使用 Swift 的Result类型,这意味着您可以清楚地看到前后。

首先,我们需要定义可以引发哪些错误。您可以定义任意多个,但在这里我们将说 URL 错误,请求失败或发生未知错误。将此枚举放在ContentView结构体之外:

代码语言:javascript
复制
enum NetworkError: Error {
    case badURL, requestFailed, unknown
}

接下来,我们将编写一个返回Result的方法。请记住,Result是为了表示某种成功或失败而设计的,在这种情况下,我们要说的是,成功案例将包含从网络返回的任何内容的字符串,而错误将是某种NetworkError

我们将四次编写相同的方法,但是会增加复杂性,因此您可以了解到底该如何使用。首先,我们将立即发送一个badURL错误,这意味着将此方法添加到ContentView中:

代码语言:javascript
复制
func fetchData(from urlString: String) -> Result<String, NetworkError> {
    .failure(.badURL)
}

如您所见,该方法的返回类型为Result <String,NetworkError>,表示成功时为字符串,失败时为NetworkError值。尽管非常快,但这仍然是一个阻塞函数调用。

我们真正想要的是一个非阻塞调用,这意味着我们无法将Result作为返回值发送回去。取而代之的是,我们需要使我们的方法接受两个参数:一个用于要获取的URL,另一个是将用值调用的完成闭包。这意味着该方法本身不返回任何内容。它的数据通过完成关闭传递回去,将来会在某个时候调用。

同样,我们将使此返回.badURL错误,以使事情变得简单。代码如下:

代码语言:javascript
复制
func fetchData(from urlString: String, completion: (Result<String, NetworkError>) -> Void) {
    completion(.failure(.badURL))
}

现在,我们有一个完成闭包的原因是我们现在可以使该方法成为非阻塞的:我们可以开始一些异步工作,使方法返回,以便其余代码可以继续,然后在稍后的任何时候调用完成闭包。

这里有一个很小的复杂性,尽管我之前已经简短地提到了它,但它变得很重要。当我们将闭包传递给函数时,Swift需要知道是立即使用它还是以后使用它。如果立即使用默认值——那么Swift很乐意运行闭包。但是,如果稍后使用它,则可能创建的闭包已被销毁并且不再存在于内存中,在这种情况下,闭包也将被销毁并且无法再运行。

为了解决这个问题,Swift让我们将闭包参数标记为@escaping,这意味着:

对于我们的方法,我们将运行一些异步工作,然后在完成后调用闭包。这可能立即发生,也可能需要几分钟。我们不在乎。关键是方法返回后,闭包仍需要保留,这意味着我们需要将其标记为@escaping。如果您担心忘记这一点,没有必要:Swift始终会拒绝构建代码,除非您添加@escaping属性。

这是我们函数的第三个版本,它使用@escaping作为闭包,因此我们可以异步调用它:

代码语言:javascript
复制
func fetchData(from urlString: String, completion: @escaping (Result<String, NetworkError>) -> Void) {
    DispatchQueue.main.async {
        completion(.failure(.badURL))
    }
}

请记住,可以在将来的任何时候调用完成闭包,并且仍然可以正常使用。

现在,对于该方法的第四个版本,我们将把Result代码与之前的URLSession代码混合。这将具有完全相同的函数签名——接受字符串和闭包,但不返回任何内容——但现在我们将以不同的方式调用完成闭包:

  1. 如果网址不正确,我们将调用 completion(.failure(.badURL))
  2. 如果我们从请求中获得有效数据,则将其转换为字符串,然后调用 completion(.success(stringData))
  3. 如果我们从请求中返回错误,我们将调用 completion(.failure(.requestFailed))
  4. 如果我们以某种方式无法获取数据或出现错误,则我们将调用 completion(.failure(.unknown))

唯一的新事物是如何将Data实例转换为字符串。如果您还记得的话,以前使用过 let data = Data(someString.utf8) ,当从Data转换为String时,代码有些相似:

代码语言:javascript
复制
let stringData = String(decoding: data, as: UTF8.self)

好的,现在是我们第四遍方法的时候了:

代码语言:javascript
复制
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闭包中,并处理所有可能的情况:

代码语言:javascript
复制
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情况都被覆盖了。

译自 Understanding Swift’s Result type

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档