[Swift]順序を保持するCollection、KeyValuePairs

Swift5.0ではKeyValuePairsというオブジェクトが使用できます。Hashと配列両方の特徴を一部持っているKeyValuePairsについてこの記事では扱います。
2019.06.24

こんにちは。モバイルアプリサービス部の田辺です。

Swift5.0 で追加された Dynamic Callable について調べていると KeyValuePairs という構造体が登場します。

Dynamic Callable の Proposal を見ると@dynamicCallable属性 を使用するために dynamicallyCall(withArguments:)またはdynamicallyCall(withKeywordArguments:)の実装を必要とするとあります。

その際に(withKeywordArguments:)の方では KeyValuePairs などのExpressibleByDictionaryLiteral protocol に conform した型が引数の型として使用できるのですが、 KeyValuePairs をあまり使っていなかったので、今回はそんな KeyValuePairs について調べたりコードを書いてみた結果を書いていきます。

環境

  • Xcode Version 10.2.1
  • Swift5.0

KeyValuePairs とは

Swift5.0 以前はDictionaryLiteralという名称でしたが Deprecated になり KeyValuePairs という名称になりました。

公式ドキュメントでは軽量な key と value がペアになった Collection と説明されています。

Dictionary 型や Array 型との比較

KeyValuePairs は順番が保証される。

Dictionary はハッシュテーブルなので高速なキー検索が行えますが、イテレートの際に順番が保証されないのに対して KeyValuePairs は順番が保証されます。

let dictionary = [0: "0", 1: "1", 2: "2", 3: "3"]
// Sequence protocolに conformしているのでループ処理に使える
for (key, value) in dictionary {
    print("key: \(key), value: \(value)")
}

// key: 1, value: 1
// key: 2, value: 2
// key: 3, value: 3
// key: 0, value: 0

// KeyValuePairsは順番が保証される
let user: KeyValuePairs = ["firstName": "Nobuyuki", "lastName": "Tanabe", "twitter": "t__nabe"]
for value in user {
    print("\(value.key): \(value.value)")
}

// firstName: Nobuyuki
// lastName: Tanabe
// twitter: t__nabe

これを利用して関数のパラメータに KeyValuePairs を使っているサンプルがドキュメントにあります。

同じことをやってみます。以下のコードでは initializer で KeyValuePairs を受け取ってブロック内でタプルの配列に変換してプロパティに代入しています。

struct Alphabet {
    var list: [(Int, String)]

    init(_ elements: KeyValuePairs<Int, String>) {
        self.list = Array(elements)
    }
}

let alphabet = Alphabet([0: "a", 1: "b", 2: "c"])
print(alphabet) // Alphabet(list: [(0, "a"), (1, "b"), (2, "c")])

添字で値にアクセスできる

順番が保証されているので KeyValuePairs は Array のように添え字で値にアクセス出来ます。また、添字でアクセスした場合はタプルで値が返ってきます。

print(user[1]) // (key: String, value: String)のタプル

Dictionary のようにキーを指定して値を取得できない。

Dictionary 型はHashableprotocol に conform していて、 キーベースのサブクリプトを提供します。

If you need an ordered collection of key-value pairs and don’t need the fast key lookup that Dictionary provides, see the KeyValuePairs type for an alternative.

ドキュメントに上記のように記載されている通り、Dictionary型はキーを指定しての値へのアクセスは高速です。

それに対して KeyValuePairs はキーを指定して値を取得することができません。

print(user["firstName"]) // Cannot subscript a value of type 'KeyValuePairs<String, String>' with an index of type 'String'

キーから値を取得したい時は指定したいキーを持つ value が何番目かを取得して添字でアクセスする必要があります。

if let index = user.firstIndex(where: { $0.key == "firstName"}) {
    let firstName = user[index].value
    print(firstName)
}

同じキーを登録できる

KeyValuePairs は Hashable protocol に conform していないので、Dictionary と違い同じキーを登録できます。

let user: KeyValuePairs = ["firstName": "Nobuyuki", "firstName": "Nobuyuki2", "lastName": "Tanabe", "twitter": "t__nabe"]
for value in user {
    print("\(value.key): \(value.value)")
}

// firstName: Nobuyuki
// firstName: Nobuyuki2
// lastName: Tanabe
// twitter: t__nabe

まとめ

  • Hashable に conform する必要がなく、高速なキー検索がいらない
  • key と Value がペアになった ordered な collection が欲しい
  • 重複するキーを持つ値を追加したい

上記のようなユースケースで使用すると便利な構造体だということがわかりました。具体的な使い方も想像出来るようになったので実務で使っていけたらと思います。