Swift 组合链错误中的 Swift `Failure` 关键字含义

Mat*_*oal 3 swift combine

假设我有一个简单的链,它从一个 HTTP 请求创建一个发布者 <T, APIManagerError>

   func run<T:Decodable>(request:URLRequest)->AnyPublisher<T, APIManagerError>{
        return URLSession.shared.dataTaskPublisher(for: request)
            .map{$0.data}
            .decode(type: T.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()// it should run mapError before this point
    }
Run Code Online (Sandbox Code Playgroud)

此代码产生此错误,因为我返回的是 Error 而不是APIManagerError.

Cannot convert return expression of type 
'AnyPublisher<T, Publishers.Decode<Upstream, Output, Coder>.Failure>' 
(aka 'AnyPublisher<T, Error>') 
to return type 'AnyPublisher<T, RestManagerError>'
Run Code Online (Sandbox Code Playgroud)

我知道要解决这个问题,我需要在.decode.

.mapError{error in 
    APIManagerError.error("Decode Fail")
}
Run Code Online (Sandbox Code Playgroud)

但我无法真正理解在“aka”部分之前的错误消息报告了什么,而是非常清楚

你如何阅读错误Publishers.Decode<Upstream, Output, Coder>.Failure?具体是什么意思.Failure?我Failure在哪里可以找到Swift 文档?

Saj*_*jon 8

由于我们正在谈论两种不同类型的错误让这里表示一个ç ompilation é RROR为CEFailurePublisher(符合Swift.Error)为PFP ublisher ˚F ailure)。

您的问题是关于CE消息的解释。

Cannot convert return expression of type 
'AnyPublisher<T, Publishers.Decode<Upstream, Output, Coder>.Failure>' 
Run Code Online (Sandbox Code Playgroud)

写出您的实现的结果返回类型func run- 无需mapError调用。编译器eraseToAnyPublisher()在函数末尾确认您的调用,以及您的泛型Output类型T。所以这涵盖了Cannot convert return expression of type 'AnyPublisher<T,. 至于Publishers.Decode<Upstream, Output, Coder>.Failure>'输出 的派生类型Failure。这在某种程度上是派生Failure类型的象征性细分。URLSession.DataTaskPublisher作为您URLSession.shared.dataTaskPublisher调用的结果,您的上游发布者最初是 类型,然后您将其转换为您调用的每个 Combine 运算符:map然后是decode。导致发布者Publishers.Decode。并且Failure类型不能被正确地“解符号化”(我缺乏正确的编译器知识来使用正确的术语)。

您使用哪个 Xcode 版本?在新的诊断架构也许能够表现出更好的错误消息。这实际上是我后来在回复中使用的原因.assertMapError(is: DecodingError.self)

您的代码mapError完成了这项工作,但它完全丢弃了有关实际错误的信息。所以我不会那样做。至少打印(记录)错误。但我仍然会做类似的事情:

声明自定义错误类型

直觉上,我们至少有两种不同类型的错误,要么是networking解码,要么是解码。但可能更多...

Cannot convert return expression of type 
'AnyPublisher<T, Publishers.Decode<Upstream, Output, Coder>.Failure>' 
Run Code Online (Sandbox Code Playgroud)

您可能需要告诉 combine 错误类型确实是DecodingError,因此我已经声明了一些fatalError对此信息有用的宏。它与Combine 有点相似setFailureType(但仅在上游发布者具有Failuretype时才有效Never,因此我们不能在这里使用它)。

施放或杀戮

public enum HTTPError: Swift.Error {
    indirect case networkingError(NetworkingError)
    indirect case decodingError(DecodingError)
}

public extension HTTPError {
    enum NetworkingError: Swift.Error {
        case urlError(URLError)
        case invalidServerResponse(URLResponse)
        case invalidServerStatusCode(Int)
    }
}
Run Code Online (Sandbox Code Playgroud)

然后在 上创建一个方便的方法Publisher,类似于setFailureType

func typeErasureExpected<T>(
    instance incorrectTypeOfThisInstance: Any,
    toBe expectedType: T.Type,
    _ file: String = #file,
    _ line: Int = #line
) -> Never {
    let incorrectTypeString = String(describing: Mirror(reflecting: incorrectTypeOfThisInstance).subjectType)
    fatalError(
        "Incorrect implementation: Expected variable '\(incorrectTypeOfThisInstance)' (type: '\(incorrectTypeString)') to be of type `\(expectedType)`",
        file, line
    )
}

func castOrKill<T>(
    instance anyInstance: Any,
    toType: T.Type,
    _ file: String = #file,
    _ line: Int = #line
    ) -> T {
    guard let instance = anyInstance as? T else {
        typeErasureExpected(instance: anyInstance, toBe: T.self, file, line)
    }
    return instance
}
Run Code Online (Sandbox Code Playgroud)

用法:

我冒昧地在你的例子中发现了更多的错误。断言例如服务器以非故障 HTTP 状态代码等进行响应。

extension Publisher {
    func assertMapError<NewFailure>(is newFailureType: NewFailure.Type) -> AnyPublisher<Output, NewFailure> where NewFailure: Swift.Error {
        return self.mapError { castOrKill(instance: $0, toType: NewFailure.self) }.eraseToAnyPublisher()
    }
}

Run Code Online (Sandbox Code Playgroud)

奖金 - 平等检查 HTTPError

如果我们的错误类型是Equatable,这确实非常有利,它使编写单元测试变得更加容易。要么我们走这Equatable条路线,要么我们可以做一些反射魔法。我将介绍这两种解决方案,但 Equatable 解决方案肯定更强大。

等价的

为了使HTTPError符合Equatable我们只需要手动使DecodingError相等。我用这个代码完成了这个:

func run<Model>(request: URLRequest) -> AnyPublisher<Model, HTTPError> where Model: Decodable {
    URLSession.shared
        .dataTaskPublisher(for: request)
        .mapError { HTTPError.NetworkingError.urlError($0) }
        .tryMap { data, response -> Data in
            guard let httpResponse = response as? HTTPURLResponse else {
                throw HTTPError.NetworkingError.invalidServerResponse(response)
            }
            guard case 200...299 = httpResponse.statusCode else {
                throw HTTPError.NetworkingError.invalidServerStatusCode(httpResponse.statusCode)
            }
            return data
    }
    .decode(type: Model.self, decoder: JSONDecoder())

    // It's unfortunate that Combine does not pick up that failure type is `DecodingError`
    // thus we have to manually tell the Publisher this.
    .assertMapError(is: DecodingError.self)
    .mapError { HTTPError.decodingError($0) }
    .eraseToAnyPublisher()
}

Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这也必须使DecodingError.ContextEquatable成为可能。

然后你可以声明这些 XCTest 助手:


extension DecodingError: Equatable {

    public static func == (lhs: DecodingError, rhs: DecodingError) -> Bool {

        switch (lhs, rhs) {

            /// `typeMismatch` is an indication that a value of the given type could not
            /// be decoded because it did not match the type of what was found in the
            /// encoded payload. As associated values, this case contains the attempted
            /// type and context for debugging.
        case (
            .typeMismatch(let lhsType, let lhsContext),
            .typeMismatch(let rhsType, let rhsContext)):
            return lhsType == rhsType && lhsContext == rhsContext

            /// `valueNotFound` is an indication that a non-optional value of the given
            /// type was expected, but a null value was found. As associated values,
            /// this case contains the attempted type and context for debugging.
        case (
            .valueNotFound(let lhsType, let lhsContext),
            .valueNotFound(let rhsType, let rhsContext)):
            return lhsType == rhsType && lhsContext == rhsContext

            /// `keyNotFound` is an indication that a keyed decoding container was asked
            /// for an entry for the given key, but did not contain one. As associated values,
            /// this case contains the attempted key and context for debugging.
        case (
            .keyNotFound(let lhsKey, let lhsContext),
            .keyNotFound(let rhsKey, let rhsContext)):
            return lhsKey.stringValue == rhsKey.stringValue && lhsContext == rhsContext

            /// `dataCorrupted` is an indication that the data is corrupted or otherwise
            /// invalid. As an associated value, this case contains the context for debugging.
        case (
            .dataCorrupted(let lhsContext),
            .dataCorrupted(let rhsContext)):
            return lhsContext == rhsContext

        default: return false
        }
    }
}

extension DecodingError.Context: Equatable {
    public static func == (lhs: DecodingError.Context, rhs: DecodingError.Context) -> Bool {
        return lhs.debugDescription == rhs.debugDescription
    }
}

Run Code Online (Sandbox Code Playgroud)

反射魔法

或者您可以在这里查看我的Gist,它根本不使用 Equatable,但可以“比较”任何不符合 Equatable 的枚举错误。

用法

CombineExpectation现在,您可以与您一起编写组合代码的单元测试并更轻松地比较错误!