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

时间:2018-07-17 09:04:44

标签: swift swift-protocols

情况:

请考虑以下内容:

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 { ...

但是,我遇到了“典型”的编译时错误:

  错误:协议“汽车”只能用作通用约束,因为   有自我或相关类型要求

我无法再声明cars: [Car]数组。如果我没有记错的话,其背后的原因是Equatable使用Self,因此将其视为同质的。

如何处理此问题? Type erasure是否可以解决该问题?

4 个答案:

答案 0 :(得分:3)

一种可能的解决方案是协议扩展,而不是运算符,它提供了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])

答案 1 :(得分:2)

这是使用类型擦除的解决方案:

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])

不知道如何使用静态属性来实现。如果我确定了,我将编辑此答案。

答案 2 :(得分: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")

答案 3 :(得分:1)

已经为一般问题提供了一些好的解决方案–如果您只想比较两个Car值是否相等,然后重载==或定义自己的相等方法,如下所示: @Vyacheslav@vadian分别是一种快速而简单的方法。但是请注意,这不是对Equatable的实际符合,因此,例如,它不会与条件符合组成–即,您将无法在不定义另一个相等的情况下比较两个[Car]值超载。

@BohdanSavych所示,解决该问题的更通用的方法是构建一个包装类型,该包装类型提供与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属性获取通用值。例如,这使我们可以一次定义制造商宝马,然后在他们生产的不同车型上重复使用该值。