Suzna Developer Blog

すずな株式会社の開発者が技術情報を発信します。

Swiftで自作structをArrayやDictionaryのように使えるようにしてみる

初めまして、Swiftがバージョンアップされる度に産まれたての子羊かのごとくガクブルしているKayです。
まぁ、容赦なくバージョンアップした後に泣いたり、感動したりと忙しい日々を送っております。
※ 現在のバージョンはSwift 3.0です。

Arrayっぽくしてみる

for...inで回してみる

ここでは好みで全てstructにしておりますが、classでも同じ事が出来るはずです!

struct MyStrings: Sequence {
    private var _array: [String] = ["string1", "string2", "string3"]
    func makeIterator() -> IndexingIterator<[String]> {
        return self._array.makeIterator()
    }
}

let strings = MyStrings()
for string in strings {     // string1
    print(string)           // string2
}                           // string3

Iteratorまで自作してもいいのですが、
実際にarrayを内包してるパターンが多いかとは思いますので少々手を抜きました(・・。)ゞ
必要メソッドはmakeIteratorだけなので、for...inで回すだけならこれで簡単に出来ます。

でもでも!
どうせなら、isEmptyとか、firstとか・・・使いたいですよね・・・?

Collectionプロトコルを実装してみる

プロトコルをSequenceではなく、Collectionにしておけば、

struct MyStrings: Collection {
    // fileprivateにしてるのは、後でextensionするからです
    fileprivate var _array: [String] = ["string1", "string2", "string3"]

    var startIndex: Int { return self._array.startIndex }
    var endIndex  : Int { return self._array.endIndex }

    func index(after i: Int) -> Int {
        return self._array.index(after: i)
    }

    subscript(index: Int) -> String {
        return self._array[index]
    }
}

let strings = MyStrings()
for string in strings {     // string1
    print(string)           // string2
}                           // string3

print(strings.isEmpty)              // false
print(strings.first)                // Optional("string1")
print(strings.contains("string1"))  // true

回せるだけでなく便利メソッドがいろいろ使えるようになるので、個人的にはこっちのほうが好きです( ̄▽ ̄)b

更に、必要な実装は増えますが需要に合わせてCollectionタイプを変えればよりスマートですね☆
実際に、本物のArrayはもっと他のCollectionを実装してますが、面倒くさいので省略します( '∇')

ちなみに、SequenceやCollection達の関係性はこんな感じ↓

                     +--------+
                     |Sequence|
                     +---+----+
                         |
                    +----+-----+
                    |Collection|
                    +----+-----+
                         |
          +--------------+-------------+
          |              |             |
          |     +--------+--------+    |
          |     |MutableCollection|    |
          |     +-----------------+    |
          |                            |
+---------+-------------+    +---------+----------------+
|BidirectionalCollection|    |RangeReplaceableCollection|
+---------+-------------+    +--------------------------+
          |
 +--------+-------------+
 |RandomAccessCollection|
 +----------------------+

Array風に書けるようにする

extension MyStrings: ExpressibleByArrayLiteral {
    init(arrayLiteral elements: String...) {
        self._array = elements
    }
}

let strings: MyStrings = ["string1", "string2", "string3"]
for string in strings {     // string1
    print(string)           // string2
}                           // string3

もう一手間加えると、更にArrayっぽくなります☆

Dictionary風にしてみる

for...inで回してみる

struct MyDictionary: Sequence {
    private var _dictionary: [String: Any] = ["a": "1", "b": 2, "c": "3"]
    func makeIterator() -> DictionaryIterator<String, Any> {
        return self._dictionary.makeIterator()
    }
}

let dictionary = MyDictionary()
for (key, value) in dictionary {    // b: 2
    print("\(key): \(value)")       // a: 1
}                                   // c: 3

これまたDictionaryを内包してる前提の手抜きコードですが、Arrayの時とIteratorが違うだけですね。
当然Dictionaryなので、順不同に出てきます。

Collectionを実装

struct MyDictionary: Collection {
    typealias Element = (key: String, value: Any)
    typealias Index   = DictionaryIndex<String, Any>

    // fileprivateにしてるのは、後々extensionするからです
    fileprivate var _dictionary: [String: Any] = ["a": "1", "b": 2, "c": "3"]

    var startIndex: Index { return self._dictionary.startIndex }
    var endIndex  : Index { return self._dictionary.endIndex }

    func index(after i: Index) -> Index {
        return self._dictionary.index(after: i)
    }

    subscript(index: Index) -> Element {
        return self._dictionary[index]
    }
}

let dictionary = MyDictionary()
for (key, value) in dictionary {    // b: 2
    print("\(key): \(value)")       // a: 1
}                                   // c: 3

print(dictionary.isEmpty)   // false
print(dictionary.count)     // 3
print(dictionary["aaa"])    // Error: Cannot subscript a value of type 'MyDictionary' with an index of type 'String'

IndexとElementが少々複雑になりましたが、これも実装的にはArrayとほぼ変わりません。 ただ、これだけだと回せてもあまりDictionaryらしい使い方は出来てないですね・・・

その他必要そうなメソッドを追加する

extension MyDictionary {
    subscript(key: String) -> Any? {
        return self._dictionary[key]
    }
}

print(dictionary["a"])      // Optional("1")
print(dictionary["d"])      // nil

これがなければ始まりませんよね。 後は地道にほしいメソッドを手動で追加してく方法しかないのかな・・・?(たぶん)

Dictionaryらしく書けるようにする

extension MyDictionary: ExpressibleByDictionaryLiteral {
    init(dictionaryLiteral elements: (String, Any)...) {
        self._dictionary = [:]
        for (key, value) in elements {
            self._dictionary[key] = value
        }
    }
}

let dictionary: MyDictionary = ["a": "1", "b": 2, "c": "3"]
for (key, value) in dictionary {    // b: 2
    print("\(key): \(value)")       // a: 1
}                                   // c: 3

一気にDictionaryっぽくなりました〜(*゚▽゚ノノ゙☆パチパチ

以上でした! これであなたもArrayやDictionaryのように扱える自作structを作ってみませんか〜?