Combine:错误处理

5,977 阅读3分钟

Publisher的结束事件有两种可能:代表正常完成的.finished和代表发生错误的.failure

Subscriber在订阅上游Publisher时,Combine要求Publisher.outputSubscriber.input的类型一致,同时也要求所接受的Failure的类型一致,否则就算Publisher只发出Output值,也无法使用这个Subscriber去接收。

这种设计提醒我们,在开发中要充分考虑对可能出现的错误的处理。

转换错误类型

enum RequestError: Error {
    case sessionError(error: Error)
}
let imageURLPublisher = PassthroughSubject<URL, RequestError>()
imageURLPublisher.flatMap { url in
    return URLSession.shared.dataTaskPublisher(for: url).map { _ in
        
    }
 }

这段代码会抛出一个错误,明确告诉我们RequestError和URLError的类型不一致:

candidate requires that the types 'RequestError' and 'URLSession.DataTaskPublisher.Failure' (aka 'URLError') be equivalent (requirement specified as 'Self.Failure' == 'P.Failure')

此时,使用mapError进行类型转换非常合适

let cancellable = imageURLPublisher.flatMap { requestURL in
    return URLSession.shared.dataTaskPublisher(for: requestURL)
        .mapError { error -> RequestError in
            return RequestError.sessionError(error: error)
        }
}.sink(receiveCompletion: { (error) in
    print("请求失败: \(String(describing: error))")
}, receiveValue: { (result) in
    let image = UIImage(data: result.data)
    print("请求成功: \(image?.description ?? "")")
})

imageURLPublisher.send(URL(string: "https://httpbin.org/image/jpeg")!)
imageURLPublisher.send(URL(string: "https://unknown.url/image")!)

两次send得到的结果如下:

请求成功: <UIImage:0x60000395d290 anonymous {239, 178} renderingMode=automatic(original)>
请求失败: failure(__lldb_expr_9.RequestError.sessionError(error: Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made."

抛出错误

Combine 为 Publisher 的 map 操作提供了一个可以抛出错误的版本:tryMap。使用 tryMap 我们就可以将处理数据时发生的错误,转变为标志事件流失败的结束事件:

["1", "2", "Swift", "4"].publisher
    .tryMap { s -> Int in
        guard let value = Int(s) else {
            throw IntError.typeError
        }
        return value
    }.print()
    .sink { _ in } receiveValue: { _ in }

输出的结果是:

receive subscription: (TryMap)
request unlimited
receive value: (1)
receive value: (2)
receive error: (typeError)

"Swift" 字符串是无法被转换为 Int 值的,使用tryMap就将这个错误抛了出来,导致整个事件流以错误结果终止,接下来的 "4" 也不再会被处理。

除了 tryMap 以外,Combine 中还有很多类似的以 try 开头的操作符,比如 tryScantryFiltertryReduce 等等。当需要在数据转换或者处理时,将事件流以错误进行终止,都可以使用对应操作的 try 版本来进行抛出,并在订阅者一侧接收到对应的错误事件。这些try*工作的思路是一致的。

从错误中恢复

开发中遇到不需要将错误信息反馈给用户的情况时,我们可以选择使用默认值来让事件流从错误中“恢复”的方式来处理。

在 Combine 里,有一些 Operator 是专门做这种处理的,比如 replaceErrorcatch。代码始终是最直观的说明方式。

使用replaceError

["1", "2", "Swift", "4"].publisher
    .tryMap { s -> Int in
        guard let value = Int(s) else {
            throw IntError.typeError
        }
        return value
    }
    .replaceError(with: -1)
    .print()
    .sink { _ in
    } receiveValue: { _ in
    }

/// 输出结果
//receive subscription: (ReplaceError)
//request unlimited
//receive value: (1)
//receive value: (2)
//receive value: (-1)
//receive finished

使用catch

["1", "2", "Swift", "4"].publisher
    .tryMap { s -> Int in
        guard let value = Int(s) else {
            throw IntError.typeError
        }
        return value
    }
    .catch { _ in
        Just(-1)
    }
    .print()
    .sink { _ in
    } receiveValue: { _ in
    }
/// 输出结果
//receive subscription: (Catch)
//request unlimited
//receive value: (1)
//receive value: (2)
//receive value: (-1)
//receive finished

两种方式的打印结果是相同的,但是实现的方式却不一样:

1. replaceError是使用单个值替换出错的值

2. catch将产生错误的publisher替换为了一个全新的publisher

这两种方式都可以实现错误替换,但是它们都在发生错误的地方中断了整个事件流,在开发中,这往往不是我们想要的结果。这种状态要怎么解决呢?我们可以组合使用一些操作符来达成目的。

["1", "2", "Swift", "4"].publisher
    .flatMap { s in
        Just(s)
            .tryMap { s -> Int in
                guard let value = Int(s) else {
                    throw IntError.typeError
                }
                return value
            }
            .catch { _ in
                Just(-1)
            }
    }
    .print()
    .sink { _ in
    } receiveValue: { _ in
    }
/// 输出结果
//receive subscription: (FlatMap)
//request unlimited
//receive value: (1)
//receive value: (2)
//receive value: (-1)
//receive value: (4)
//receive finished

在响应式异步编程中,使用 flatMap 进行“包装”的手法是很常见的。这样即使发生了 类似例子当中的"Swift" 无法转换为 Int 的错误,最终的 "4" 依然可以得到正确的处理。