Swift Digest
SE-0131 | Swift Evolution

Add AnyHashable to the standard library

Proposal
SE-0131
Authors
Dmitri Gribenko
Review Manager
Chris Lattner
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift 3.0 では SE-0116 により、Objective-C の id が Swift の Any としてインポートされるようになりました。この変更に伴い、それまで [NSObject : AnyObject] としてインポートされていた NSDictionary * も、値側を Any に置き換える必要があります。

ここで問題になるのがキーの型です。AnyHashable に適合していないため、そのまま [Any : Any] とすることはできません。DictionarySet のキーに使える、「任意の Hashable な値を格納でき、それ自身も Hashable である」ような型消去コンテナが標準ライブラリに存在しないというのが根本的な課題でした。

Swift 2 系では NSObject が便宜的にキーの型として使われていましたが、NSObject は参照型かつ Objective-C ランタイムに依存する型であり、Swift ネイティブの値型(IntString、ユーザー定義の構造体など)を自然に扱える受け皿としてはふさわしくありません。Any ベースのブリッジングを徹底するためには、Swift の世界で完結した汎用的な型消去 Hashable が必要でした。

02 どのように解決されるのか

標準ライブラリに、任意の Hashable な値を包んで保持する型消去コンテナ AnyHashable を追加します。AnyHashable 自体が Hashable に適合しているため、Dictionary のキーや Set の要素として直接使えます。これにより、アノテーションのない NSDictionary[AnyHashable : Any] としてインポートされるようになります。

AnyHashable の基本形

AnyHashable は、Hashable に適合する任意の値を init(_:) で包み、base: Any プロパティで元の値を取り出せる構造体です。

public struct AnyHashable {
    public init<H : Hashable>(_ base: H)
    public var base: Any { get }
}

extension AnyHashable : Equatable, Hashable {
    public static func == (lhs: AnyHashable, rhs: AnyHashable) -> Bool
    public var hashValue: Int { get }
}

等価性とハッシュ値は、包まれた元の値のものに転送されます。異なる型の値は別物として扱われる点に注意が必要です。

let x = AnyHashable(Int(42))
let y = AnyHashable(UInt8(42))
print(x == y)                   // false: Int と UInt8 は別の型
print(x == AnyHashable(Int(42))) // true

混在した型をキーにする

AnyHashable を使うと、異なる型の値を同じ Dictionary のキーとして混在させられます。

let descriptions: [AnyHashable : Any] = [
    AnyHashable("😄"): "emoji",
    AnyHashable(42): "an Int",
    AnyHashable(Int8(43)): "an Int8",
    AnyHashable(Set(["a", "b"])): "a set of strings",
]

print(descriptions[AnyHashable(42)]!)          // "an Int"
print(descriptions[AnyHashable(43)] as Any)    // nil (Int としての 43 は未登録)
print(descriptions[AnyHashable(Int8(43))]!)    // "an Int8"

利便性のためのオーバーロード

毎回 AnyHashable(...) で包むのは冗長なので、Set<AnyHashable>Dictionary<AnyHashable, _> には、具体的な Hashable 値を直接渡せる便利APIが追加されます。

func contains42(_ data: Set<AnyHashable>) -> Bool {
    // 明示的に包まなくても、具体値をそのまま渡せる
    return data.contains(42)
}

Set<AnyHashable> には contains / index(of:) / insert / update(with:) / remove の、Dictionary<AnyHashable, _> には index(forKey:) / subscript / updateValue(_:forKey:) / removeValue(forKey:) の、それぞれ具体的な Hashable 型を受け取るオーバーロードが用意されます。

使いどころ

AnyHashable の主な目的は、Objective-C との橋渡しで NSDictionary などを自然に扱えるようにすることです。通常の Swift コードで Dictionary のキーを混在させる必要はほとんどないので、ジェネリクスで型を明示できる場面では具体型のまま使うほうが安全かつ高速です。AnyHashable は、型を静的に決めきれないブリッジ境界や、ヘテロな値をひとまとめに扱いたい特殊な用途のための道具だと考えるとよいでしょう。