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

Swift `Failure` keyword meaning in a Swift Combine chain error

假设我有一个简单的链,它从一个 HTTP 请求为 <T, APIManagerError>

创建了一个发布者
   func run<T:Decodable>(request:URLRequest)->AnyPublisher<T, APIManagerError>{
        return URLSession.shared.dataTaskPublisher(for: request)
            .map{[=12=].data}
            .decode(type: T.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()// it should run mapError before this point
    }

此代码产生此错误,因为我返回错误而不是 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>'

我知道要解决这个问题,我需要在 .decode 之后添加一个 mapError。

.mapError{error in 
    APIManagerError.error("Decode Fail")
}

但我无法真正理解 "aka" 部分之前的错误消息报告的内容,而是相当清楚

错误Publishers.Decode<Upstream, Output, Coder>.Failure怎么看? .Failure 部分具体是什么意思?我在哪里可以找到 Swift 文档中的 Failure

实际上我很容易就找到了答案,但我将问题悬而未决,以防其他人需要它。

本例中的Failure关键字就是Publisher协议附带的associatedtype

在这种情况下,我只是获取 Publishers.DecodeFailure 类型,默认情况下只是 Error

因为我们在这里讨论两种不同类型的错误,所以我们将 C编译 E 错误表示为 CEPublisher(符合 Swift.Error)的 Failure 作为 PF (P发布者 F失败)。

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

Cannot convert return expression of type 
'AnyPublisher<T, Publishers.Decode<Upstream, Output, Coder>.Failure>' 

写出您实现 func run 的结果返回类型 - 没有 mapError 调用。编译器会在函数末尾确认您对 eraseToAnyPublisher() 的调用,以及类型 T 的泛型 Output。这样就涵盖了 Cannot convert return expression of type 'AnyPublisher<T,。至于Publishers.Decode<Upstream, Output, Coder>.Failure>'输出的是Failure的派生类型。这在某种程度上是派生 Failure 类型的符号分解。您的上游 Publisher 最初是 URLSession.DataTaskPublisher, as a result of your URLSession.shared.dataTaskPublisher call, which you then transform with every Combine operator you call: map and then decode. Resulting in the publisher Publishers.Decode 类型。 Failure 类型不能正确 "desymbolised" (我缺乏正确的编译器知识来使用正确的术语)。

您使用哪个 Xcode 版本? New Diagnostic Architecture 可能能够显示更好的错误消息。这实际上就是我后来在回复中使用 .assertMapError(is: DecodingError.self)

的原因

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

声明自定义错误类型

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

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)
    }
}

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

castOrKill

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
}

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

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

用法:

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

func run<Model>(request: URLRequest) -> AnyPublisher<Model, HTTPError> where Model: Decodable {
    URLSession.shared
        .dataTaskPublisher(for: request)
        .mapError { HTTPError.NetworkingError.urlError([=14=]) }
        .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([=14=]) }
    .eraseToAnyPublisher()
}

奖金 - HTTPError

的等式检查

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

平等

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


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
    }
}

如您所见,还必须使 DecodingError.Context 等价。

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

    func XCTAssertThrowsSpecificError<ReturnValue, ExpectedError>(
        file: StaticString = #file,
        line: UInt = #line,
        _ codeThatThrows: @autoclosure () throws -> ReturnValue,
        _ error: ExpectedError,
        _ message: String = ""
    ) where ExpectedError: Swift.Error & Equatable {

        XCTAssertThrowsError(try codeThatThrows(), message, file: file, line: line) { someError in
            guard let expectedErrorType = someError as? ExpectedError else {
                XCTFail("Expected code to throw error of type: <\(ExpectedError.self)>, but got error: <\(someError)>, of type: <\(type(of: someError))>")
                return
            }
            XCTAssertEqual(expectedErrorType, error, line: line)
        }
    }

    func XCTAssertThrowsSpecificError<ExpectedError>(
        _ codeThatThrows: @autoclosure () throws -> Void,
        _ error: ExpectedError,
        _ message: String = ""
    ) where ExpectedError: Swift.Error & Equatable {
        XCTAssertThrowsError(try codeThatThrows(), message) { someError in
            guard let expectedErrorType = someError as? ExpectedError else {
                XCTFail("Expected code to throw error of type: <\(ExpectedError.self)>, but got error: <\(someError)>, of type: <\(type(of: someError))>")
                return
            }
            XCTAssertEqual(expectedErrorType, error)
        }
    }

    func XCTAssertThrowsSpecificErrorType<Error>(
        _ codeThatThrows: @autoclosure () throws -> Void,
        _ errorType: Error.Type,
        _ message: String = ""
    ) where Error: Swift.Error & Equatable {
        XCTAssertThrowsError(try codeThatThrows(), message) { someError in
            XCTAssertTrue(someError is Error, "Expected code to throw error of type: <\(Error.self)>, but got error: <\(someError)>, of type: <\(type(of: someError))>")
        }
    }

反射魔法

或者你可以看看我的Gist here,它根本不使用 Equatable,但可以 "compare" 任何不符合 Equatable 的枚举错误。

用法

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