例如,我需要从不同来源获取两个相同类型的数组。这两个请求都可能会失败,但是当这些请求同时失败时,我需要显示任何错误(由服务器给出)。
这是一个常见的示例,但当任何请求失败时它都会抛出错误:
func test() async throws -> [Any] {
async let t1 = getArr()
async let t2 = getArr2()
return try await t1 + t2
}
func getArr() async throws -> [Any] {
[]
}
func getArr2() async throws -> [Any] {
[]
}
同时,如果我使用
try?
,我会丢失请求返回的错误。
我也可以删除
throws
并用 Result<[Any], Error>
替换返回结果,但看起来很奇怪。
如何正确解决这个问题?
你绝对应该在这里使用
Result
。为了方便起见,首先编写一个 Result
初始化程序来捕获异步抛出闭包中的错误。
extension Result {
init(asyncCatching block: () async throws -> Success) async where Failure == Error {
do {
self = .success(try await block())
} catch {
self = .failure(error)
}
}
}
假设您希望
test
在所有 async let
抛出时抛出所有错误,请将 [Error]
也设为 Error
类型。
extension Array: Error where Element: Error {}
那么就很简单:
func test() async throws -> [Any] {
async let t1 = Result { try await getArr() }
async let t2 = Result { try await getArr2() }
switch (await t1, await t2) {
case let (.failure(e1), .failure(e2)):
return [e1, e2]
case let (.success(s), .failure), let (.failure, .success(s)):
return s
case let (.success(s1), .success(s2)):
return s1 + s2
}
}
对于“将
Result
的集合减少为单个值或抛出”操作的更通用版本,您可以编写:
func accumulateResults<T, U, Failure: Error>(_ results: [Result<T, Failure>], identity: U, reduce: (U, T) -> U) throws -> U {
var ret = identity
var success = false
var failures = [Failure]()
for result in results {
switch result {
case .success(let s):
ret = reduce(ret, s)
case .failure(let e):
if success { break }
failures.append(e)
}
}
return if success { ret } else { throw failures }
}
如果不使用
Result
(或重新发明类似的东西),test
只能抛出最后一个错误,因为你无法存储任何以前的错误(Result
的确切工作)。
func testWithoutResult() async throws -> [Any] {
async let t1 = getArr()
async let t2 = getArr2()
var result = [Any]()
var success = false
do {
result.append(try await t1)
success = true
} catch {}
do {
result.append(try await t2)
} catch {
if !success { throw error }
}
return result
}
对于每个非最终异步操作,编写
do
块并设置 success = true
。对于最终的异步操作,请改为在 catch 块中if !success { throw error }
。
虽然 Swift 并发性最大限度地减少了我们对
Result
类型的依赖(有自己的、更自然的抛出错误和返回成功的方式),但 Result
类型仍然受到非常多的支持。
例如,
Task
有自己的result
属性。因此,在这种情况下我可能会利用它并返回成功/失败值的数组:
func test() async -> [Result<[Int], Error>] {
let t1 = Task { try await getArr() }
let t2 = Task { try await getArr2() }
return await withTaskCancellationHandler {
await [t1.result, t2.result]
} onCancel: {
t1.cancel()
t2.cancel()
}
}
func getArr() async throws -> [Int] {
if Bool.random() {
return [1, 2, 3]
} else {
throw RandomChanceError.badLuck
}
}
func getArr2() async throws -> [Int] {
if Bool.random() {
return [4, 5, 6]
} else {
throw RandomChanceError.badLuck
}
}
enum RandomChanceError: Error {
case badLuck
}
非结构化并发的唯一技巧是您有责任添加取消处理,如上面使用
withTaskCancellationHandler
所示。 (您当前可能不会预期取消此 test
方法,但在编写非结构化并发时始终添加取消处理仍然是谨慎的做法。)
无论如何,调用者可以迭代这些结果:
for result in await test() {
switch result {
case .success(let value): print(value)
case .failure(let error): print(error)
}
}
在您的示例中,您将返回
[Any]
。 getArr
和 getArr2
真的每个都返回异构数组吗?或者您使用 [Any]
因为这两个函数实际上只是返回两个不同类型的不同同构数组?
如果后者是真的,我可能建议退休
[Any]
,因为这实际上并不能反映两个独立同质数组的性质。例如,在下面,我考虑了一个场景,其中第一个(我已重命名为getNumbers
)返回一种类型的同构数组,第二个(我已重命名为getStrings
)返回一个同构数组,但类型不同:
func test() async throws {
let numbersAndStrings = try await getNumbersAndStrings()
switch numbersAndStrings.numbers {
case .success(let numbers): print(numbers)
case .failure(let error): print(error)
}
switch numbersAndStrings.strings {
case .success(let strings): print(strings)
case .failure(let error): print(error)
}
}
func getNumbersAndStrings() async throws -> (numbers: Result<[Int], Error>, strings: Result<[String], Error>) {
let t1 = Task { try await getNumbers() }
let t2 = Task { try await getStrings() }
return try await withTaskCancellationHandler {
let results = await (
numbers: t1.result,
strings: t2.result
)
try Task.checkCancellation()
return results
} onCancel: {
t1.cancel()
t2.cancel()
}
}
func getNumbers() async throws -> [Int] {
try await Task.sleep(for: .seconds(.random(in: 1...3)))
if Bool.random() {
return [1, 2, 3]
} else {
throw SomeError.badLuck
}
}
func getStrings() async throws -> [String] {
try await Task.sleep(for: .seconds(.random(in: 1...3)))
if Bool.random() {
return ["four", "five", "six"]
} else {
throw SomeError.badLuck
}
}
enum SomeError: Error {
case badLuck
}