How can I conform a Swift enum to `Equatable` when it has an any existential as one of its associated values?

huangapple go评论51阅读模式
英文:

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) -&gt; 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&lt;Error: MyError&gt;) 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) -&gt; Bool

    init&lt;E: Equatable&gt;(_ value: E) {
        self.value = value
        self.equals = { type(of: $0) == type(of: value) &amp;&amp; $0 as? E == value }
    }

    static func == (lhs: AnyEquatable, rhs: AnyEquatable) -&gt; 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&lt;B: Equatable&gt;(to b: B) -&gt; 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) -&gt; 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&lt;T: MyError&gt;() -&gt; T? {
        if case let .error(error as T) = self { return error }
        else { return nil }
    }
}

, and assert on that:

XCTAssertEqual(state.wrappedError(), TestError.error1)

huangapple
  • 本文由 发表于 2023年2月24日 10:20:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/75552090.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定