使用类型擦除的协议数组无法符合Hashable或Equatable协议。

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

Array of protocols using type erasure cannot conform to Hashable or Equatable protocols

问题

I'm running into an issue wherein the pattern I am attempting to implement does not play nicely with SwiftUI statefulness and refuses to update structs that conform to my TestProtocol when they are in an array.

我遇到了一个问题,我尝试实现的模式与SwiftUI的状态管理不太兼容,并且在它们在数组中时拒绝更新符合TestProtocol的结构体。

I am chalking this up to my fundamental misunderstanding so would appreciate any guidance on the matter.

我认为这是因为我根本没有理解清楚,所以希望能得到一些关于这个问题的指导。

Here is a sanitized code snippet

以下是一个经过精简的代码片段

protocol TestProtocol: Identifiable, Hashable, Equatable  {
    var id: any Identifiable { get }
}

struct TestStruct: Identifiable, Hashable, Equatable {
    let id: UUID = UUID()

    let testArray: [any TestProtocol]

    public func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(testArray) /// ERROR: Type 'any TestProtocol' cannot conform to 'Hashable'
    }

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id && lhs.testArray == rhs.testArray /// ERROR: Type 'any TestProtocol' cannot conform to 'Equatable'
    }
}

class TestObservable: ObservableObject {
    static let shared = TestObservable()

    @Published var filters: [TestStruct]

    init() {
        self.filters = [
            TestStruct(
                testArray: [
                    ...
                ]
            )
        ]
    }
}

EDIT: Thank you for the responses so far, I'm starting to understand that the structure I am aiming for may be breaking some rules. If so, here is the goal I was driving for in the hopes that providing additional clarity might help in suggesting a possible solution.

编辑:感谢迄今为止的回复,我开始理解我所追求的结构可能会违反一些规则。如果是这样的话,这是我追求的目标,希望提供额外的明确信息可能会有助于提出可能的解决方案。

protocol TestProtocol: Identifiable, Hashable, Equatable  {
    var id: UUID { get }
}

protocol ATestProtocol: TestProtocol  {
    var aValue: String { get set }
}

protocol BTestProtocol: TestProtocol  {
    var bValue: Bool { get set }
}

struct ATestStruct: ATestProtocol {
    let id = UUID()
    var aValue: String = ""
}

struct BTestStruct: BTestProtocol {
    let id = UUID()
    var bValue: Bool = false
}

struct TestStruct: Identifiable, Hashable, Equatable {
    let id: UUID = UUID()

    let testArray: [any TestProtocol]

    public func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(testArray)
    }

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id && lhs.testArray == rhs.testArray
    }
}

class TestObservable: ObservableObject {
    static let shared = TestObservable()

    @Published var filters: [TestStruct]

    init() {
        self.filters = [
            TestStruct(
                testArray: [
                    ATestStruct(),
                    BTestStruct()
                ]
            )
        ]
    }
}

Please note that the provided code contains some errors and inconsistencies, which you may need to address to achieve your desired functionality.

英文:

I'm running into an issue wherein the pattern I am attempting to implement does not play nicely with SwiftUI statefulness and refuses to update structs that conform to my TestProtocol when they are in an array.

I am chalking this up to my fundamental misunderstanding so would appreciate any guidance on the matter.

Here is a sanitized code snippet

protocol TestProtocol: Identifiable, Hashable, Equatable  {
    var id: any Identifiable { get }
}

struct TestStruct: Identifiable, Hashable, Equatable {
    let id: UUID = UUID()

    let testArray: [any TestProtocol]

    public func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(testArray) /// ERROR: Type 'any TestProtocol' cannot conform to 'Hashable'
    }

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id && lhs.testArray == rhs.testArray /// ERROR: Type 'any TestProtocol' cannot conform to 'Equatable'
    }
}

class TestObservable: ObservableObject {
    static let shared = TestObservable()

    @Published var filters: [TestStruct]

    init() {
        self.filters = [
            TestStruct(
                testArray: [
                    ...
                ]
            )
        ]
    }
}

EDIT: Thank you for the responses so far, I'm starting to understand that the structure I am aiming for may be breaking some rules. If so, here is the goal I was driving for in the hopes that providing additional clarity might help in suggesting a possible solution.

protocol TestProtocol: Identifiable, Hashable, Equatable  {
    var id: UUID { get }
}

protocol ATestProtocol: TestProtocol  {
    var aValue: String { get set }
}

protocol BTestProtocol: TestProtocol  {
    var bValue: Bool { get set }
}

struct ATestStruct: ATestProtocol {
    let id = UUID()
    var aValue: String = ""
}

struct BTestStruct: BTestProtocol {
    let id = UUID()
    var bValue: Bool = false
}

struct TestStruct: Identifiable, Hashable, Equatable {
    let id: UUID = UUID()

    let testArray: [any TestProtocol]

    public func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(testArray)
    }

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id && lhs.testArray == rhs.testArray
    }
}

class TestObservable: ObservableObject {
    static let shared = TestObservable()

    @Published var filters: [TestStruct]

    init() {
        self.filters = [
            TestStruct(
                testArray: [
                    ATestStruct(),
                    BTestStruct()
                ]
            )
        ]
    }
}

答案1

得分: 2

你正在构建的看起来不错。您只需要手动构建方法来生成哈希值和比较值。默认实现将不起作用。您需要遍历数组中的每个项。

生成哈希值应该很简单:

public func hash(into hasher: inout Hasher) {
    hasher.combine(id)
    testArray.forEach { item in
        hasher.combine(item)
    }
}

现在比较器变得有点复杂。首先,我们需要检查数组大小是否相同。我们可以确定如果一个项目的元素少于另一个项目,则它们不相同。
然后,我们需要将每个元素彼此比较。为此,我们需要尝试将一个对象转换为另一个对象的类型,然后进行比较。我们需要双向进行比较,因为如果 A 可转换为 B 并不意味着 B 可以转换为 A,这是由于子类化导致的。

我设法构建了以下内容。这不是最佳的代码块,但应该能够解释需要完成的工作:

static func == (lhs: Self, rhs: Self) -> Bool {
    guard lhs.id == rhs.id else { return false }
    
    let leftArray = lhs.testArray
    let rightArray = rhs.testArray
    
    guard leftArray.count == rightArray.count else { return false }
    
    for index in 0..<leftArray.count {
        let leftItem = leftArray[index]
        let rightItem = rightArray[index]
        
        func checkIsOriginalEqualToTarget<OriginalType: Equatable, TargetType: Equatable>(original: OriginalType, to target: TargetType) -> Bool {
            guard let converted = original as? TargetType else { return false }
            return converted == target
        }
        
        if checkIsOriginalEqualToTarget(original: leftItem, to: rightItem) {
            continue // Looks good
        } else if checkIsOriginalEqualToTarget(original: rightItem, to: leftItem) {
            continue // Looks good
        } else {
            return false
        }
    }
    return true
}

希望这能帮助您完成您的工作。

英文:

What you are building looks fine. You only need to manually build your methods to generate hash and to compare values. The default implementations simply will not do. You need to iterate through each of your items within the array.

It should be simple for generating a hash:

public func hash(into hasher: inout Hasher) {
    hasher.combine(id)
    testArray.forEach { item in
        hasher.combine(item)
    }
}

Now the comparator becomes a bit more complex. We first need to check if array sizes are the same. We are pretty sure if one item has fewer elements than the other then they are not the same.
Then we need to compare each element with one another. To do so we need to attempt to convert one object into type of the other and then compare them. And we need to do it both ways because if A is convertible to B it does not mean that B is convertible to A due to subclassing.

I managed to build the following. Not the best chunk of code but it should be best to explain what needs to be done:

static func == (lhs: Self, rhs: Self) -&gt; Bool {
    guard lhs.id == rhs.id else { return false }
    
    let leftArray = lhs.testArray
    let rightArray = rhs.testArray
    
    guard leftArray.count == rightArray.count else { return false }
    
    for index in 0..&lt;leftArray.count {
        let leftItem = leftArray[index]
        let rightItem = rightArray[index]
        
        func checkIsOriginalEqualToTarget&lt;OriginalType: Equatable, TargetType: Equatable&gt;(original: OriginalType, to target: TargetType) -&gt; Bool {
            guard let converted = original as? TargetType else { return false }
            return converted == target
        }
        
        if checkIsOriginalEqualToTarget(original: leftItem, to: rightItem) {
            continue // Looks good
        } else if checkIsOriginalEqualToTarget(original: rightItem, to: leftItem) {
            continue // Looks good
        } else {
            return false
        }
    }
    return true
}

Answering old part of the question and explaining why it can not work as easily

To completely strip down your problem imagine the following method:

func areTheseItemsTheSame(_ items: [any Equatable]) -&gt; Bool {
    guard items.isEmpty == false else { return true }
    let anyItem = items[0]
    return !items.contains(where: { $0 != anyItem })
}

It looks useful. I could easily check things like areTheseItemsTheSame([&quot;1&quot;, &quot;2&quot;, &quot;1&quot;, &quot;1&quot;, &quot;1&quot;]) or areTheseItemsTheSame([true, true, false]) and so on.

But what if I would use it like areTheseItemsTheSame([&quot;1&quot;, true])? Both values are equatable but they can not be equated amongst themselves.

So we need to restrict this method with generic:

func areTheseItemsTheSame&lt;ItemType: Equatable&gt;(_ items: [ItemType]) -&gt; Bool {
    guard items.isEmpty == false else { return true }
    let anyItem = items[0]
    return !items.contains(where: { $0 != anyItem })
}

This now compiles and makes sense. Calling it with different types will produce an error that it has conflicting types. There is even a shorthand for this using some keyword. So you can do

func areTheseItemsTheSame(_ items: [some Equatable]) -&gt; Bool {
    guard items.isEmpty == false else { return true }
    let anyItem = items[0]
    return !items.contains(where: { $0 != anyItem })
}

To apply the same solution a bite more information would be nice on what your result should look like but something like the following should work:

protocol TestProtocol: Identifiable, Hashable, Equatable  {
    
}

struct TestStruct&lt;ItemType: TestProtocol&gt;: Identifiable, Hashable, Equatable {
    let id: UUID = UUID()

    let testArray: [ItemType]

    public func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(testArray)
    }

    static func == (lhs: Self, rhs: Self) -&gt; Bool {
        lhs.id == rhs.id &amp;&amp; lhs.testArray == rhs.testArray
    }
}

And I can now use pretty much anything to insert into this structure. For instance

extension String: TestProtocol {
    
    public var id: String { self }
    
}

class TestObservable: ObservableObject {
    static let shared = TestObservable()

    @Published var filters: [TestStruct&lt;String&gt;]

    init() {
        self.filters = [
            TestStruct(testArray: [&quot;String&quot;, &quot;Test&quot;])
        ]
    }
}

I hope this sets you on the right path.

huangapple
  • 本文由 发表于 2023年5月11日 08:55:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76223465.html
匿名

发表评论

匿名网友

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

确定