Open1

Swift: Arrayのcontainsでclassが含まれているか判定する

kabeyakabeya

SwiftのArraycontainsを使って、その配列内にすでに特定のオブジェクトが含まれているか判定することを考えます。

オブジェクトが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 {に書き換えるだけでItemEquatableプロトコルに準拠します。

詳細はよく分かりませんが、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の場合
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の場合
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になります。