Skip to content

Latest commit

 

History

History
315 lines (231 loc) · 8.4 KB

12 - 错误管理.md

File metadata and controls

315 lines (231 loc) · 8.4 KB

本文是阅读 Combine: Asynchronous Programming with Swift 后的学习笔记,有需要的请点击链接购买。

12 - 错误管理

Never

Failure 类型为 Never 的 publisher 表示该 publisher 永远不会发出错误。Failure 类型为 Never 的 publisher 允许您集中精力使用 publisher 的值,同时确保 publisher 永远不会发出错误。

例如 Just publisher:

Just("Hello")

Command-click 点进去查看定义你会发现,它的 Failure 类型为 Never

public typealias Failure = Never

Combine 提供了一些只能在不发出错误的 publisher 上使用的操作符。例如:

Just("Hello")
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

运行结果为:

Hello

在上面的例子中,我们使用 sink(receiveValue:),它是 sink 的其中一个重载方法,并且他只能在不发出错误的 publisher 上使用。Combine 非常智能,如果是会发出错误的 publisher,它会强制你使用另外一个带有 receiveCompletion 的操作符。

为了验证它,我们先来看一个操作符 setFailureType

setFailureType

可以还是用 setFailureType 来把一个不发出错误的 publisher 变成会发出错误的 publisher。

例如:

enum MyError: Error {
    case ohNo
}

Just("Hello")
    .setFailureType(to: MyError.self)
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .failure(.ohNo):
                print("Finished with Oh No!")
            case .finished:
                print("Finished successfully!")
            }
    },
        receiveValue: { value in
            print("Got value: \(value)")
    })

运行结果:

Got value: Hello
Finished successfully!

例子中虽然使用 setFailureType 修改了 Failure 的类型,并且使用 sink 的带有 receiveCompletion 的重载方法来订阅 Just。如果改为用另外一个重在方法 sink(receiveValue:),则会报错:

Referencing instance method 'sink(receiveValue:)' on 'Publisher' requires the types 'MyError' and 'Never' be equivalent

要求 FailureNever

assign(to:on:)

这个操作符也是仅适用于不会发出错误的 publisher,用于把 publisher 发出的值赋给某个对象的的某个属性。

例如:

class Person {
    let id = UUID()
    var name = "Unknown"
}

let person = Person()

print("1", person.name)

Just("Shai")
    .handleEvents(
        receiveCompletion: { _ in print("2", person.name) }
    )
    .assign(to: \.name, on: person)

运行结果:

1 Unknown
2 Shai

assertNoFailure

当希望在开发过程中保护自己并确认 publisher 无法以发出错误事件而结束时,assertNoFailure 操作符非常有用。它不会阻止上游发出错误事件。但是,如果它检测到一个错误,它将以 fatalError 的形式 crash。

例如:

Just("Hello")
    .setFailureType(to: MyError.self)
    .assertNoFailure()
    .sink(receiveValue: { print("Got value: \($0) ")})

运行结果:

Got value: Hello

如果在 assertNoFailure 前面加上:

.tryMap { _ in throw MyError.ohNo }

那么,运行之后将会报错。

这个操作符在开发调试的时候非常有用。

处理错误

try 系列操作符

在 Combine 中,所有 try 前缀的运算符在出现错误时的行为都相同。所以这里只以 tryMap 为例。

首先看以下例子:

let names = ["Scott", "Marin", "Shai", "Florent"].publisher

names
    .map { $0.count }
    .sink(
        receiveCompletion: { print("Completed with \($0)") },
        receiveValue: { print("Got value: \($0)") }
    )

运行结果:

Got value: 5
Got value: 5
Got value: 4
Got value: 7
Completed with finished

上面代码中,使用 map 把字符串转换为他的长度。

接下来我们有一个需求:如果字符串的长度小于 5,则抛出错误。我们把 map 里面的代码修改如下:

enum NameError: Error {
    case tooShort(String)
    case unknown
}

.map { value -> Int in
    let length = value.count
    guard length >= 5 else {
        throw NameError.tooShort(value)
    }
    return value.count
}

然而,我们看到一个编译错误:

Invalid conversion from throwing function of type '(_) throws -> Int' to non-throwing function type '(String) -> _'

因为 map 是一个不会跑出错误的方法,所以不能在方法里面抛出错误。这时我们的 tryMap 就派上用场了。

map 改为 tryMap,完整代码为:

enum NameError: Error {
    case tooShort(String)
    case unknown
}

let names = ["Scott", "Marin", "Shai", "Florent"].publisher

names
    .tryMap { value -> Int in
        let length = value.count
        guard length >= 5 else {
            throw NameError.tooShort(value)
        }
        return value.count
    }
    .sink(
        receiveCompletion: { print("Completed with \($0)") },
        receiveValue: { print("Got value: \($0)") }
    )

运行结果如下:

Got value: 5
Got value: 5
Completed with failure(__lldb_expr_23.NameError.tooShort("Shai"))

那么如果处理抛出的错误呢?继续往下看。

Map error

首先看一下例子:

// 定义错误类型
enum NameError: Error {
    case tooShort(String)
    case unknown
}

Just("Hello")
    .setFailureType(to: NameError.self) // 把 publisher 的错误类型改为 NameError
    .map { $0 + " World!" }
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Done!")
            case .failure(.tooShort(let name)):
                print("\(name) is too short!")
            case .failure(.unknown):
                print("An unknown name error occurred")
            }
    },
        receiveValue: { print("Got value \($0)") }
    )

运行结果为:

Got value Hello World!
Done!

receiveCompletion 闭包的 completion 参数上进行 Option-click,你会发现 Completion 的错误类型为 NameError,而这就是我们想要的错误类型。

如果我们把 .map { $0 + " World!" } 改为 .tryMap { $0 + " World!" },并再次对 receiveCompletion 闭包的 completion 参数上进行 Option-click,你会发现 Completion 的错误类型变成了 Swift 的 Error 类型,即使我们没有在 tryMap 中抛出错误。并且 switch 也无法正常进行了。

造成这个现象的原因是 Swift 并不支持抛出指定的错误类型,也就意味着,如果你使用 try 系列的操作符,抛出的错误类型都是 Swift 的 Error 类型。

这是我们可以使用 mapError 包通用的 Error 类型转换成我们自定义的错误类型。本例中在 tryMap 后面加上:

.mapError { $0 as? NameError ?? .unknown }

mapErrorError 转换成了 NameError 类型。

捕获和重试

假设有一个 PhotoService 类,可以用于获取图片。

我们通过下面的例子来演示如何使用 catchretry

var subscriptions = Set<AnyCancellable>()

let photoService = PhotoService()

photoService
    .fetchPhoto(quality: .high) // 获取高质量的图片
    .retry(3) // 如果获取失败,则重试三次
    .catch { error -> PhotoService.Publisher in
        print("Failed fetching high quality, falling back to low quality")
            return photoService.fetchPhoto(quality: .low) // 获取低质量的图片
    }
    .replaceError(with: UIImage(named: "na.jpg")!)
    .sink(
        receiveCompletion: { print("\($0)") },
        receiveValue: { image in
            image
            print("Got image: \(image)")
    }
)
    .store(in: &subscriptions)  // 异步的订阅需要保存起来

解释代码逻辑:

  • 首先通过 fetchPhoto(quality: .high) 获取高质量的图片。
  • 如果获取失败,则通过 retry(3) 重试 3 次。
  • 如果重试三次后还是失败,则 catch 会捕获到错误,然后通过 photoService.fetchPhoto(quality: .low) 去获取低质量的图片。
  • 如果获取低质量的图片还是失败,replaceError 就会把错误替换成一个默认的图片,相当于我们在给 UIImageView 设置图片时,通常会设置一个 placeholder 图片。
  • 最后通过 sink 订阅。