了解 Swift 的 Result 类型

2020-09-14 11:17:01 浏览数 (1)

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

0 人点赞