英文:
How can I conform a Swift enum to `Equatable` when it has an any existential as one of its associated values?
问题
假设我有:
protocol MyError: Error, Equatable {
var errorDispalyTitle: String { get }
var errorDisplayMessage: String { get }
}
enum ContentState {
case .loading
case .error(any MyError)
case .contentLoaded
}
如果我想在ContentState
中实现Equatable
,以便在单元测试中进行比较,我会遇到问题,因为我必须比较两个any MyError
类型,它们是封装的,可能具有两种不同的基础类型。
extension ContentState: Equatable {
static func == (lhs: ContentState, rhs: ContentState) -> Bool {
switch (lhs, rhs) {
case (.loading, .loading):
return true
case (.contentLoaded, .contentLoaded):
return true
case (.error(let lhsError), .error(let rhsError)):
// TODO: 必须比较两个基础类型是否匹配,然后再比较它们的值是否相等
default:
return false
}
}
}
我该如何做呢?
我尝试将存在类型的泛型移到类型本身(例如,ContentState<Error: MyError>
),这样在实现Equatable
时可以推断出类型,但问题是对于使用该枚举的任何类来说,接收的类型无关紧要,只要是任何类型就可以了。如果我不实现any
存在类型,它开始要求将泛型传递到整个链中。
英文:
Suppose I have:
protocol MyError: Error, Equatable {
var errorDispalyTitle: String { get }
var errorDisplayMessage: String { get }
}
enum ContentState {
case .loading
case .error(any MyError)
case .contentLoaded
}
If i were to implement Equatable
in ContentState
so I can compare during unit tests I end up with a problem because I have to compare two any MyError
types which are boxed and might be of two different underlying types.
extension ContentState: Equatable {
static func == (lhs: ContentState, rhs: ContentState) -> Bool {
switch (lhs, rhs) {
case (.loading, .loading):
return true
case (.contentLoaded, .contentLoaded):
return true
case (.error(let lhsError), .error(let rhsError)):
// TODO: have to compare if both underlying types are match and then if they are equal in value
default:
return false
}
}
}
How do I do this?
I tried lifting the generic from the existential type there to the type (e.g. ContentState<Error: MyError>
) which lets it compile when implementing equatable as it knows how to infer the type, but the problem is for whichever class uses that enum it doesnt matter which type is receiving of it, only that is any type of it, and if I don't implement the any
existential it starts requiring the generic to be propagated up the chain.
答案1
得分: 2
你可以编写一个包装器,用于包装Equatable
,然后在比较之前包装LHS和RHS错误。
// 与AnyHashable类似的行为...
class AnyEquatable: Equatable {
let value: Any
private let equals: (Any) -> Bool
init<E: Equatable>(_ value: E) {
self.value = value
self.equals = { type(of: $0) == type(of: value) && $0 as? E == value }
}
static func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool {
lhs.equals(rhs.value)
}
}
然后在你的switch
语句中可以这样做:
case (.error(let lhsError), .error(let rhsError)):
return AnyEquatable(lhsError) == AnyEquatable(rhsError)
请注意,如果MyError
继承自Hashable
而不是Equatable
,你可以使用内置的 AnyHashable
而不是编写自己的 AnyEquatable
。
英文:
You can write a wrapper that wraps an Equatable
, and wrap the LHS and RHS errors before comparing.
// resembling a similar behaviour to AnyHashable...
class AnyEquatable: Equatable {
let value: Any
private let equals: (Any) -> Bool
init<E: Equatable>(_ value: E) {
self.value = value
self.equals = { type(of: $0) == type(of: value) && $0 as? E == value }
}
static func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool {
lhs.equals(rhs.value)
}
}
Then in your switch you can do:
case (.error(let lhsError), .error(let rhsError)):
return AnyEquatable(lhsError) == AnyEquatable(rhsError)
Note that if MyError
inherits from Hashable
instead of Equatable
, you can use the builtin AnyHashable
instead of writing your own AnyEquatable
.
答案2
得分: 1
截止到 Swift 5.7,当你将存在类型作为泛型类型的参数传递时,Swift 会自动“打开”它。隐式的 self
参数可以被打开(实际上,Swift 一直都会打开 self
参数),而 Swift 可以在单个调用中打开多个参数。因此,我们可以编写一个 isEqual(to:)
函数,用于比较任何 Equatable
类型的对象:
extension Equatable {
func isEqual<B: Equatable>(to b: B) -> Bool {
return b as? Self == self
}
}
然后我们可以按照以下方式完成你的 ContentState
遵守 Equatable
:
extension ContentState: Equatable {
static func == (lhs: ContentState, rhs: ContentState) -> Bool {
switch (lhs, rhs) {
case (.loading, .loading):
return true
case (.contentLoaded, .contentLoaded):
return true
case (.error(let lhsError), .error(let rhsError)):
return lhsError.isEqual(to: rhsError)
default:
return false
}
}
}
英文:
As of Swift 5.7, Swift automatically “opens” an existential when you pass it as an argument of generic type. The implicit self
argument can be opened (in fact Swift has always opened the self
argument), and Swift can open multiple arguments in a single invocation. So we can write an isEqual(to:)
function that compares any Equatable
to any other Equatable
like this:
extension Equatable {
func isEqual<B: Equatable>(to b: B) -> Bool {
return b as? Self == self
}
}
And then we can complete your ContentState
conformance like this:
extension ContentState: Equatable {
static func == (lhs: ContentState, rhs: ContentState) -> Bool {
switch (lhs, rhs) {
case (.loading, .loading):
return true
case (.contentLoaded, .contentLoaded):
return true
case (.error(let lhsError), .error(let rhsError)):
return lhsError.isEqual(to: rhsError)
default:
return false
}
}
}
答案3
得分: 0
对于单元测试,我建议尽可能地扩展你的类型以包含Bool
值:
extension ContentState {
var isLoading: Bool {
if case .loading = self { return true }
else { return false }
}
var isError: Bool {
if case .error = self { return true }
else { return false }
}
var isContentLoaded: Bool {
if case .contentLoaded = self { return true }
else { return false }
}
}
因为我认为你主要是关心测试某个对象是否正确地更新其状态。
这样做可以更容易编写和阅读单元测试,并更好地传达意图。
如果你还想显式检查错误,你可以利用泛型:
extension ContentState {
func wrappedError<T: MyError>() -> T? {
if case let .error(error as T) = self { return error }
else { return nil }
}
}
然后进行断言:
XCTAssertEqual(state.wrappedError(), TestError.error1)
英文:
Four unit testing, I would recommend extending your type with Bool
's as much as possible:
extension ContentState {
var isLoading: Bool {
if case .loading = self { return true }
else { return false }
}
var isError: Bool {
if case .error = self { return true }
else { return false }
}
var isContentLoaded: Bool {
if case .contentLoaded = self { return true }
else { return false }
}
}
, as I assume you're mostly interested in testing if some object properly updates its state.
This makes unit tests easier to write and read, and they better transmit the intent.
If you want to also explicitly check the error, you could make use of generics:
extension ContentState {
func wrappedError<T: MyError>() -> T? {
if case let .error(error as T) = self { return error }
else { return nil }
}
}
, and assert on that:
XCTAssertEqual(state.wrappedError(), TestError.error1)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论