案子:
考虑以下:
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 { ...
但是,我得到了“典型的”编译时错误:
错误:协议'Car'只能用作通用约束,因为它具有Self或关联类型要求
我不能再声明cars: [Car]
数组了。如果我没有误会,背后的原因是Equatable
使用Self
所以它被认为是同质的。
我该如何处理这个问题? Type erasure可以成为解决它的机制吗?
已经给出了对一般问题的一些好的解决方案 - 如果你只是想要一种方法来比较两个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
在这里,我们利用Hashable
为Swift 4.1引入的自动SE-0185合成,它将考虑所有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
属性来获得常见值。这让我们举例说明制造商宝马一次,然后重新使用他们制造的不同车型的价值。一种可能的解决方案是协议扩展,而不是运算符,它提供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])
这是使用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])
不知道如何使用static属性实现它。如果我弄明白,我会编辑这个答案。
可以覆盖==
:
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")