はじめに
JSONなどを用いてデータを受け渡ししていると、しばしば様々な型のキーとバリューが組み合さったDictionaryを作りたくなるときがある。しかし、Dictionary<AnyKey, AnyObject>
のようなものを使ってしまうと、危険なダウンキャストを行わなれけばならず、プログラムがランタイムに異常終了する可能性が高まる。そこで、この記事ではいろいろな型のキーやバリューが格納できる安全なDictionaryであるHDic(Heterogeneous Dictionary)を作ることを目標とする。この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は、関係R
がConcreteRelation
であり、キーがString
でバリューもString
である組のアクセスを可能にするための仕組みであり、下はキーとバリューがそれぞれInt
とInt
の組に対するものである。 このように、extensionを追加すれば、既存のInt
やString
以外のプログラマが定義した型についても設定できる。 そして、正しくアクセス制御ができているかを確認する。
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
型の値true
でget
を実行してもコンパイルエラーとなる。
他のアクセスとの同居
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を作ることができた。ややボイラープレートが残ってしまったが、これを用いればより信頼性の高いプログラムを書くことができるかもしれない。
コメント
コメントを投稿