いろいろな型のキーとバリューが使える“安全”なDictionaryを実装する

はじめに

JSONなどを用いてデータを受け渡ししていると、しばしば様々な型のキーとバリューが組み合さったDictionaryを作りたくなるときがある。しかし、Dictionary<AnyKey, AnyObject>のようなものを使ってしまうと、危険なダウンキャストを行わなれけばならず、プログラムがランタイムに異常終了する可能性が高まる。そこで、この記事ではいろいろな型のキーやバリューが格納できる安全なDictionaryであるHDic(Heterogeneous Dictionary)を作ることを目標とする。このHDicはプログラマが入れられるキーとバリューの型を制御することができ、許可した型のキーとバリューはどれだけでも入れたり出したりできる一方で、許可のないものでアクセスした場合はコンパイルに失敗するというものになっている。 この記事を読んで分からないことや、疑問点や改善するとよい部分を見つけた場合は、コメントなどで気軽に教えて欲しい。 なお、この記事で公開されているコードは次のリポジトリから入手できる。

https://github.com/y-yu/HDic

Dictionary[AnyKey, AnyObject]

まず安全ではないが、いろいろな型のキーとバリューがを出し入れできるDictionaryを作ることにする。 SwiftはキーがプロトコルHashableを実装していなければならない。まずはStackOverflowの記事を参考に、次のようなHashableの実装を用意する。

// see http://stackoverflow.com/questions/24119624/how-to-create-dictionary-that-can-hold-anything-in-key-or-all-the-possible-type
struct AnyKey: Hashable {
    var underlying: Any
    var hashValueFunc: () -> Int
    var equalityFunc: (Any) -> Bool
    
    init(_ key: T) {
        underlying = key
        hashValueFunc = { key.hashValue }
        equalityFunc = {
            if let other = $0 as? T {
                return key == other
            }
            return false
        }
    }
    
    var hashValue: Int { return hashValueFunc() }
}

func == (x: AnyKey, y: AnyKey) -> Bool {
    return x.equalityFunc(y.underlying)
}

これは次のようにすることで、HashableなものをAnyKeyにすることができる。

var a = AnyKey(1)
var b = AnyKey("string")
var c = AnyKey(true)

これを使えば、なんら型安全ではないが、ひとまずあらゆる型のキーとバリューを挿入できるDictionaryを次のように定義できる。

var dic = Dictionary()
dic[AnyKey("string")] = true
dic[AnyKey(true)]     = 1

しかし、これは型情報が失われて全部がAnyObjectになってしまうので、次のようにダウンキャストを使って取り出すしかない。

dic[AnyKey("string")] as! Bool

アクセス可能なキーとバリューの組を表すRelation

危険なダウンキャストを避けるために、まず次のようなプロトコルを導入する。

protocol Relation {
    associatedtype Me = Self
    associatedtype Key
    associatedtype Value
}

これは、自身の型Meとキーとバリューの型の組を表す簡単なプロトコルである。これを用いて、型安全なDictionaryであるHDicを定義する。

安全なDictionaryであるHDicの定義

次のように定義する。

struct HDic {
    let underlying: Dictionary
    
    internal init(_ dic: Dictionary = Dictionary()) {
        underlying = dic
    }

    func _get(k: K) -> Optional {
        return(underlying[AnyKey(k)] as? V)
    }
    
    func _add(k: K, v: V) -> HDic {
        var n = self.underlying
        n.updateValue(v as! AnyObject, forKey: AnyKey(k))
        return HDic(n)
    }
}

HDicは基本的にはDictionary<AnyKey, AnyObject>のラッパーである。また、_get_addはどんな型のキーやバリューでも入れたり出したりできるので、これらをそのまま使うと危険なことになってしまう。そこで、この_get_addのラッパーを用意する。

HDicへの安全なアクセス

HDicが取る型パラメータRとは、さきほど作ったプロトコルRelationの実装である。これは次のように適当に実装すればよい。

struct ConcreteRelation: Relation {
    typealias Key = Any
    typealias Value = Any
}

そして、これを用いてHDicを初期化する。

var h1 = HDic()

次のようにextensionを使ってHDicへのアクセスを許可する型を決める。

extension HDic where R.Me == ConcreteRelation, R.Key == String, R.Value == String {
    func get(k: String) -> Optional {
        return _get(k)
    }
    
    func add(k: String, v: String) -> HDic {
        return _add(k, v: v)
    }
}

extension HDic where R.Me == ConcreteRelation, R.Key == Int, R.Value == Int {
    func get(k: Int) -> Optional {
        return _get(k)
    }
    
    func add(k: Int, v: Int) -> HDic {
        return _add(k, v: v)
    }
}

まず、上に書かれたextensionは、関係RConcreteRelationであり、キーがStringでバリューもStringである組のアクセスを可能にするための仕組みであり、下はキーとバリューがそれぞれIntIntの組に対するものである。 このように、extensionを追加すれば、既存のIntString以外のプログラマが定義した型についても設定できる。 そして、正しくアクセス制御ができているかを確認する。

var h1 = HDic()
var h2 = h1.add("string", v: "string")
var h3 = h2.add(2, v: 1)
var h4 = h3.add("str", v: true)  // compile time error!
h3.get(true) // compile time error!

print(h3.get("string")) // Optional("string")

このように、先ほど定義したキーがStringでバリューもStringなものと、キーがIntでバリューもIntなもの以外は挿入することができないし、またはBool型の値truegetを実行してもコンパイルエラーとなる。

他のアクセスとの同居

ConcreteRelationとは違った種類のアクセスを同居させる場合は、次のようにすればよい。まず、もうひとつのキーとバリューの型の関係を定義する。

struct AnotherRelation: Relation {
    typealias Key = Any
    typealias Value = Any
}

定義内容はConcreteRelationと全く同じだが、名前が異なるのでConcreteRelationとは別物である。そして、extensionを次のように定義する。

extension HDic where R.Me == AnotherRelation, R.Key == Bool, R.Value == Bool {
    func get(k: Bool) -> Optional {
        return _get(k)
    }

    func add(k: Bool, v: Bool) -> HDic {
        return _add(k, v: v)
    }
}

これは、関係がAnotherRelationであり、キーの型がBoolでバリューの型がBoolであるようなアクセスを許可する。したがって、次のような動作となる。

var h1 = HDic()
var h2 = h1.add(true, v: false) // compile time error!

var h3 = HDic()
var h4 = h3.add(true, v: false) // ok

このように、ConcreteRelationとは別の制御ができることが確認できる。このようにすることで、異なるアクセスを同居させることができる。

まとめ

このように、extensionを用いて、特定のキーとバリューの型だけがアクセスできるような安全なDictionaryを作ることができた。ややボイラープレートが残ってしまったが、これを用いればより信頼性の高いプログラムを書くことができるかもしれない。

コメント