Help us understand the problem. What is going on with this article?

自作クラスのインスタンスの Array からは、contains() や indexOf() を呼ぶ際に不便

More than 1 year has passed since last update.

問題点の整理

Swift で String の Array 内に特定の文字列があるか否か、または Array 内での オブジェクトの index を求める場合は、こんな感じで書けます。

var names = ["Tokyo", "New York", "London"]
let found = names.contains("Tokyo") // true
let index = names.indexOf("London") // 2

ところが、自作のクラスの Array で同じような事をしようとすると‥

class Item {
    var name: String
    init(_ name: String) { self.name = name }
}

let apple = Item("Apple")
let banana = Item("Banana")
let orange = Item("Orange")
var items = [apple, banana, orange]

let found = items.contains(apple) // error
let index = items.indexOf(banana) // error

以下のようなエラーになってしまいます。

Cannot convert value of type 'Item' to expected argument type '@noescape (Item) throws -> Bool'

原因は、ItemEquatable プロトコルに準拠していないからです。そこで、以下のようにクロージャー内に評価式を書く方法もありますが...

let found = items.contains({ return $0.name == apple.name }) // true
let index = items.indexOf({ return $0.name == banana.name }) // 1

(その1)Equatable プロトコルに準拠させる

そこはやっぱり、ItemEquatable プロトコルに準拠させてあげます。その為に、Equatable の記述と、func == 関数を用意します。この関数は、右辺と左辺を評価して、等価か否かを評価して Bool を戻すものです。この場合は、名前が一致すれば等価としてみました。

class Item: Equatable {                 // Equatable
    var name: String
    init(_ name: String) { self.name = name }
}

func == (l: Item, r: Item) -> Bool {    // 評価関数
    return l.name == r.name
}

let apple = Item("Apple")
let banana = Item("Banana")
let orange = Item("Orange")
var items = [apple, banana, orange]

let found = items.contains(apple) // true
let index = items.indexOf(banana) // 1
let index = items.indexOf(Item("Orange")) // 2

(その2)NSObject を基底クラスとする

自作クラスを NSObject のサブクラスにする方法も考えられます。

class Item: NSObject {
    var name: String
    init(_ name: String) { self.name = name }
}

let apple = Item("Apple")
let banana = Item("Banana")
let orange = Item("Orange")
var items = [apple, banana, orange]

let found = items.contains(apple) // true
let index = items.indexOf(banana) // 1

注目は、NSObject。あまり手をかけずに、contains(element)indexOf(element) が使えるようになりますが、このままでは、オブジェクトの名前ではなくオブジェクトが同一かどうかで判断されてしまうので、以下のような書き方をすると、異なる結果になってしまいます。

let found = items.contains(Item("Apple")) // false <-- !?

これは、いい悪いではなく、こういうものだと理解する必要があります。この挙動を変えたい場合は、== 評価関数を書いてあげます。プロパティが多いと大変そうですね。

func == (l: Item, r: Item) -> Bool {
    return l.name == r.name
}

ちなみに、オブジェクトが同一かどうかで評価する場合は以下のように書きます。

func == (l: Item, r: Item) -> Bool {
    return l === r
}

さらに、structenumの場合は、NSObject のサブクラス化による方法は無理っぽい事は言うまでもありませんね。

【環境】
Xcode 7.2, Swift 2.1.1

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away