如何评估同质集合的平等性?
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,然后在他们制造的不同车型中重复使用该值。
案例:
考虑以下几点:
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
值是否相等,那么重载 ==
或定义你自己的相等方法,如分别由 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,然后在他们制造的不同车型中重复使用该值。