Swift: Arrayのcontainsでclassが含まれているか判定する
SwiftのArray
でcontains
を使って、その配列内にすでに特定のオブジェクトが含まれているか判定することを考えます。
struct
の場合
オブジェクトがstruct Item {
var name: String
var price: Double
}
struct ItemList {
var items: [Item] = []
func add(_ item: Item) {
items.append(item)
}
func hasItem(_ item: Item) -> Bool {
// ↓エラー:Cannot convert value of type 'Item' to expected argument type '(Item) throws -> Bool', Missing argument label 'where:' in call
return items.contains(item)
// ↓エラー:Referencing operator function '==' on 'Equatable' requires that 'Item' conform to 'Equatable'
return items.contains(where: { $0 == item })
}
}
最初のエラーは「contains(item)
という書き方はできませんよ、contains(where:)
を使ってね」ですが、次のエラーは「==
を使うならEquatable
プロトコルに準拠しなさいよ」ということですね。
今回の場合、struct Item {
をstruct Item: Equatable {
に書き換えるだけでItem
はEquatable
プロトコルに準拠します。
詳細はよく分かりませんが、Equatable
の定義を見ると、「automatic synthesis(自動合成)」という機能があり、struct
の場合は、すべてのストアドプロパティがEquatable
プロトコルに準拠していれば使える、ということのようです。
この場合の自動合成で作られる==
の実装は、左辺と右辺のオブジェクトのすべてのストアドプロパティが==
ならtrue
、そうでなければfalse
を返す、というもののようです。
Equatable
プロトコルに準拠すると、contains(item)
という書き方もできるようになります。
class
の場合
オブジェクトがEquatable
の定義には、Equality is Separate From Identifyと書いてあります。「同値性は同一性から分離されています」というようなことですね。
値型(struct
)の場合は、どのオブジェクト変数も異なる実体を指しています。
この場合は、同じかどうかは中身の値が同じかどうかで判断するしかありません。
参照型(class
)の場合は、複数の変数が同じ実体を指していることがあります。
逆に同じ値を持っていても、参照先の実体は異なっているということがあります。
同じかどうかは、
- 同じ値を持っている
- 同じ実体を指している
の2通りの考え方ができる、と言えます。
class Item {
var name: String
var price: Double
}
struct ItemList {
var items: [Item] = []
func add(_ item: Item) {
items.append(item)
}
func hasItem(_ item: Item) -> Bool {
// ↓エラー:Cannot convert value of type 'Item' to expected argument type '(Item) throws -> Bool', Missing argument label 'where:' in call
return items.contains(item)
// ↓エラー:'Item' is not convertible to 'AnyHashable'
return items.contains(where: { $0 == item })
}
}
最初のエラーはstruct
同様、「contains(item)
という書き方はできませんよ、contains(where:)
を使ってね」ですが、次のエラーは「==
を使うならAnyHashable
プロトコルに準拠しなさいよ」ということで、struct
の場合のEquatable
と異なり、AnyHashable
となりました。
このAnyHashable
のハッシュ値比較というのは、定義を見るとちょっと魔法のようです。
let descriptions: [AnyHashable: Any] = [
42: "an Int",
43 as Int8: "an Int8",
["a", "b"] as Set: "a set of strings"
]
print(descriptions[42]!) // prints "an Int"
print(descriptions[42 as Int8]!) // prints "an Int"
print(descriptions[43 as Int8]!) // prints "an Int8"
print(descriptions[44]) // prints "nil"
print(descriptions[["a", "b"] as Set]!) // prints "a set of strings"
定義に書いてあるサンプルをそのまま転記しました。
形無しでのハッシュ値、ということなんですが、2つ目のprint
なんかは、どうやって見つけてきているのかという気になります。
さてこのAnyHashable
のブラックボックスさ加減もさることながら、class
なのに、値で同値を判定するというのが(個人的には)違和感があります。
2つの参照先が同一の実体かどうかで判定する場合は、==
ではなく、===
で比較します。
つまり、return items.contains(where: { $0 == item })
→return items.contains(where: { $0 === item })
にするとエラーは解消され、同じ実体のオブジェクトが含まれるかどうかが判定されるようになります。
実行結果
struct Item: Equatable {
var name: String
var price: Double
}
struct ItemList {
var items: [Item] = []
mutating func add(_ item: Item) {
items.append(item)
}
func hasItem(_ item: Item) -> Bool {
return items.contains(item)
}
}
let apple1 = Item(name: "apple", price: 100)
let apple2 = Item(name: "apple", price: 100)
let apple3 = apple1
var itemList = ItemList()
itemList.add(apple1)
print("has apple1?: \(itemList.hasItem(apple1))") // has apple1?: true
print("has apple2?: \(itemList.hasItem(apple2))") // has apple2?: true
print("has apple3?: \(itemList.hasItem(apple3))") // has apple3?: true
class Item {
var name: String
var price: Double
init(name: String, price: Double) {
self.name = name
self.price = price
}
}
struct ItemList {
var items: [Item] = []
mutating func add(_ item: Item) {
items.append(item)
}
func hasItem(_ item: Item) -> Bool {
return items.contains(where: { $0 === item })
}
}
let apple1 = Item(name: "apple", price: 100)
let apple2 = Item(name: "apple", price: 100)
let apple3 = apple1
var itemList = ItemList()
itemList.add(apple1)
print("has apple1?: \(itemList.hasItem(apple1))") // has apple1?: true
print("has apple2?: \(itemList.hasItem(apple2))") // has apple2?: false
print("has apple3?: \(itemList.hasItem(apple3))") // has apple3?: true
struct
は同値かどうかで判定しているので、2個目もtrue
ですが、class
は同一実体かどうかで判定しているので、2個目はfalse
になります。