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.Decode
的 Failure
类型,默认情况下只是 Error
。
因为我们在这里讨论两种不同类型的错误,所以我们将 C编译 E 错误表示为 CE
和Publisher
(符合 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 代码的单元测试并更轻松地比较错误!
假设我有一个简单的链,它从一个 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.Decode
的 Failure
类型,默认情况下只是 Error
。
因为我们在这里讨论两种不同类型的错误,所以我们将 C编译 E 错误表示为 CE
和Publisher
(符合 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 代码的单元测试并更轻松地比较错误!