如何评估同质集合的平等性?

How to evaluate equality for homogeneous collections?

案例:

考虑以下几点:

protocol Car {
    static var country: String { get }
    var id: Int { get }
    var name: String { get set }
}

struct BMW: Car {
    static var country: String = "Germany"
    var id: Int
    var name: String
}

struct Toyota: Car {
    static var country: String = "Japan"
    var id: Int
    var name: String
}

这里我有一个简单的例子,说明如何使用 -Car- 协议创建一个抽象层,因此我能够声明一个异构的汽车集合:

let cars: [Car] = [BMW(id: 101, name: "X6"), Toyota(id: 102, name: "Prius")]

而且效果很好。

问题:

我希望能够评估汽车的平等性(通过id),示例:

cars[0] != cars[1] // true

所以,我试图做的是让 Car 符合 Equatable 协议:

protocol Car: Equatable { ...

但是,我遇到了 "typical" 编译时错误:

error: protocol 'Car' can only be used as a generic constraint because it has Self or associated type requirements

我无法再声明 cars: [Car] 数组。如果我没记错的话,其背后的原因是 Equatable 使用 Self 所以它会被认为是同类的。

我该如何处理这个问题? Type erasure 可以作为一种解决机制吗?

一个可能的解决方案是协议扩展,它提供了一个 isEqual(to 函数

而不是运算符
protocol Car {
    static var country: String { get }
    var id: Int { get }
    var name: String { get set }
    func isEqual(to car : Car) -> Bool
}

extension Car {
    func isEqual(to car : Car) -> Bool {
        return self.id == car.id
    }
}

并使用它

cars[0].isEqual(to: cars[1])

可以覆盖 ==:

import UIKit

var str = "Hello, playground"
protocol Car {
    static var country: String { get }
    var id: Int { get }
    var name: String { get set }
}

struct BMW: Car {
    static var country: String = "Germany"
    var id: Int
    var name: String
}

struct Toyota: Car {
    static var country: String = "Japan"
    var id: Int
    var name: String
}

func ==(lhs: Car, rhs: Car) -> Bool {
    return lhs.id == rhs.id
}

BMW(id:0, name:"bmw") == Toyota(id: 0, name: "toyota")

这是使用 Type Erasure 的解决方案:

protocol Car {
    var id: Int { get }
    var name: String { get set }
}

struct BMW: Car {
    var id: Int
    var name: String
}

struct Toyota: Car {
    var id: Int
    var name: String
}

struct AnyCar: Car, Equatable {
    private var carBase: Car

    init(_ car: Car) {
        self.carBase = car
    }

    var id: Int { return self.carBase.id }
    var name: String {
        get { return carBase.name}
        set { carBase.name = newValue }
    }

    public static func ==(lhs: AnyCar, rhs: AnyCar) -> Bool {
        return lhs.carBase.id == rhs.carBase.id
    }
}



let cars: [AnyCar] = [AnyCar(BMW(id: 101, name: "X6")), AnyCar(Toyota(id: 101, name: "Prius"))]

print(cars[0] == cars[1])

不知道如何用静态实现这个属性。如果我弄明白了,我会编辑这个答案。

已经给出了一些解决一般问题的好方法——如果你只是想要一种方法来比较两个 Car 值是否相等,那么重载 == 或定义你自己的相等方法,如分别由 and 显示,是一种快速简单的方法。但是请注意,这并不是 Equatable 的实际符合性,因此不会构成例如条件符合性——即您将无法在不定义另一个相等性的情况下比较两个 [Car] 值过载。

所示,该问题的更通用的解决方案是构建一个提供与 Equatable 一致性的包装器类型。这需要更多的样板文件,但通常组合得更好。

值得注意的是,无法将具有关联类型的协议用作实际类型只是 Swift 语言的当前限制——这一限制可能会在 generalised existentials 的未来版本中解除.

然而,在这种情况下,考虑是否可以重组您的数据结构以消除对协议的需求,这通常有助于消除相关的复杂性。与其将各个制造商建模为单独的类型,不如将制造商建模为一种类型,然后在单个 Car 结构上拥有这种类型的 属性 怎么样?

例如:

struct Car : Hashable {
  struct ID : Hashable {
    let rawValue: Int
  }
  let id: ID

  struct Manufacturer : Hashable {
    var name: String
    var country: String // may want to consider lifting into a "Country" type
  }
  let manufacturer: Manufacturer
  let name: String
}

extension Car.Manufacturer {
  static let bmw = Car.Manufacturer(name: "BMW", country: "Germany")
  static let toyota = Car.Manufacturer(name: "Toyota", country: "Japan")
}

extension Car {
  static let bmwX6 = Car(
    id: ID(rawValue: 101), manufacturer: .bmw, name: "X6"
  )
  static let toyotaPrius = Car(
    id: ID(rawValue: 102), manufacturer: .toyota, name: "Prius"
  )
}

let cars: [Car] = [.bmwX6, .toyotaPrius]
print(cars[0] != cars[1]) // true

这里我们利用 SE-0185 中为 Swift 4.1 引入的自动 Hashable 综合,它将考虑 Car 的所有存储属性平等。如果你想改进它以只考虑 id,你可以提供你自己的 ==hashValue 的实现(只要确保强制执行不变量,如果 x.id == y.id,那么所有其他属性都相等)。

鉴于合规性很容易合成,IMO 在这种情况下没有真正的理由只符合 Equatable 而不是 Hashable

在上面的例子中还有一些其他值得注意的事情:

  • 使用 ID 嵌套结构来表示 id 属性 而不是普通的 Int。对这样的值执行 Int 操作没有意义(减去两个标识符是什么意思?),并且您不希望能够将汽车标识符传递给例如期望的东西披萨标识符。通过将值提升为它自己的强嵌套类型,我们可以避免这些问题(Rob Napier 有a great talk that uses this exact example)。

  • 使用方便的 static 属性获取通用值。例如,这让我们可以一次定义制造商 BMW,然后在他们制造的不同车型中重复使用该值。