Equatable
に適合したインスタンス同士を比較したい場合が時々あります。以下のコードでいうとGenericItem
同士を比較できると、いろいろ便利な場面がありますが、残念ながら ==
オペレータで単純に比較する事はできません。
protocol GenericItem: Hashable {
associatedtype Element
var value: Element { get }
}
struct StringItem: GenericItem {
var value: String
}
struct IntItem: GenericItem {
var value: Int
}
let item1: any GenericItem = StringItem(value: "hello")
let item2: any GenericItem = StringItem(value: "hello")
let item3: any GenericItem = StringItem(value: "world")
let item4: any GenericItem = IntItem(value: 24)
// Binary operator '==' cannot be applied to two 'any GenericItem' operands
item1 == item2 // error
item1 == item2 // error
item2 == item3 // error
item3 == item4 // error
この例で言うなら、GenericItem
にisEqual(to:)
の類いのメソッドを用意して比較するなんて方法もあるようです。
protocol GenericItem: Equatable {
func isEqual(to other: any GenericItem) -> Bool
}
extension GenericItem {
func isEqual(to other: any GenericItem) -> Bool {
if let other = other as? Self {
return self == other
}
return false
}
}
func compare(_ item1: any GenericItem, _ item2: any GenericItem) -> Bool {
// error: Binary operator '==' cannot be applied to two 'any GenericItem' operands
// return item1 == item2
// OK
return item1.isEqual(to: item2)
}
が、今回もっと簡単な方法を見つけたので、紹介したいと思います。
func == (lhs: any Equatable, rhs: any Equatable) -> Bool {
return ([lhs] as NSArray) == ([rhs] as NSArray)
}
上記の呪文を唱えれば、以下のように、any Equatable
同士が比較できるようになります。
let item1: any Equatable = StringItem(value: "hello")
let item2: any Equatable = StringItem(value: "hello")
let item3: any Equatable = StringItem(value: "world")
let item4: any Equatable = IntItem(value: 24)
item1 == item2 // true
item1 == item2 // true
item2 == item3 // false
item3 == item4 // false
この例では、StringItem
もIntItem
もNSObject
のサブクラスでもNSObjectProtocol
に適合しているわけでもありませんが、NSArray
同士を比較するときはメンバーのany Equatable
同士をなぜかいい感じで比較してくれるようです。
内部の仕組みがわかれば、NSArray
とかでラップしなくても、良い方法で比較できるかもしれません。
さて、一部の人には、any Equatable
同士を比較したいのに、なぜ冒頭のコードはHashable
になっているのかと気になった人もいるかもしれません、不思議なことにHashable
をEquatable
にすると、挙動が変わります。
any Equatable
同士を比較したいのに、対象のインスタンスは Hashable
に適合していないと挙動が変わってしまうという不思議さです。
protocol GenericItem: Hashable { // <-- Hashable
associatedtype Element
var value: Element { get }
}
//...snip...
([item1] as NSArray) == ([item2] as NSArray) // true
([item2] as NSArray) == ([item3] as NSArray) // false
protocol GenericItem: Equatable { // <-- Equatable
associatedtype Element
var value: Element { get }
}
//...snip...
([item1] as NSArray) == ([item2] as NSArray) // false <-- !?
([item2] as NSArray) == ([item3] as NSArray) // false
ひょっとして、まだ見知らぬ罠が潜んでいる可能性はありますが、一つの方法として紹介したいと思います。
$ swift --version
swift-driver version: 1.62.15 Apple Swift version 5.7.1 (swiftlang-5.7.1.135.3 clang-1400.0.29.51)
Target: arm64-apple-macosx13.0