なんとなくHackerTackleで喋った内容について思い出したので、共有します。
端的に述べると、引数の型がAnyの場合にOptionalな値を突っ込むと死ぬよって話です。
結論
Any?を使いましょう
まずはじめに
任意の型からStringを取り出す関数a
を考えます。
func a(value: Any) -> String? {
if let string = value as? String {
return string
}
return nil
}
value
がString
ならそれを返す。違えばnil
。簡単ですね。
ではこのa
に次のb
を引数として与えましょう。
let b: String? = "text"
a(b) // == nil
なぜでしょうか?b
には確かに"text"を入れたはずです。
Optionalはただのnil許可ではない
まずOptional
はnil許可ではなく、構造です。
Optional
の宣言を見ればわかるとおり、enumで表現された構造です。
swift/Optional.swift at master · apple/swift
a
に渡った時点で、b
はよろしく解釈されて生のString
、若しくはnil
としてAny
にラップされるわけではなく、Optional<String>
としてAny
にラップされます。
asは単なるキャストではない
Swiftにおけるキャストであるところのas
ですが、これは単純なキャストではありません。
型によって、振る舞いが変わります。
左項が単純な型の場合は、キャストします。その型が合致するとキャストに成功します。
左項がOptional
の場合は、Optional
が内包する型に対してキャストします。Some(Wrapped)
であり、かつWrapped
が合致するとキャストに成功します。
管見の及ぶ範囲でas
の振る舞いはもう一種類凶悪なのがありますが、それについては割愛します。また後日紹介します。
Anyに隠蔽されたOptionalを考える
では、このOptional
の有無による振る舞いはいつ決まるのでしょうか?
答えはコンパイル時です。従って、Any
にOptional
が隠蔽されていることをas
は知りません。
なるほどAny
に対して合致する型のみキャストが成功するわけで、この場合Any
に隠蔽された型はOptional<String>
ですから、String
へのキャストは成功しません。
さっきのa
を、Optional<String>
へのキャストに書き換えれば動作しそうです。しかし、Optional
へのキャストを直接書くと、SorceKitは大激怒です。困った。
if let string = value as? String? { // Cannot downcast from 'Any' (aka 'protocol<>') to a more optional type 'String?'
return string
}
こういう場合は、一旦Generics funcに型を渡して、SorceKitに認識させないテクニックが有効です。
func cast<A>(value: Any, type: A.Type) -> A? {
return value as? A
}
if let string = cast(value, type: Optional<String>.self) {
return string // == "text"
}
動きましたね。値も無事返ってきました。
暗黙の型変換
しかし釈然としません。こんな面倒臭いプログラムは書きたくない。
さて、今日のSwiftにおいては、幾つかの暗黙の型変換が存在します。例えば、任意の型A
は、スーパーセットであるB
にas
無しで型変換が可能です。
特にOptional
に関しては幾つかの暗黙の型変換があるので、覚えておくと役に立ちます。
1. A -> B => A -> B?
2. A? -> B => A -> B
3. A? => B? (A is a B)
1と2は、クロージャの型の一部がOptional
へ変換されるということです。これについてはまた別の機会に。
重要なのは3で、A
のOptional
は、スーパーセットであるB
のOptional
へ暗黙の型変換が可能なのです。
さて、Any
は任意のプロトコルなので、全ての型のスーパーセットでありますから、任意のOptional
は、Optional<Any>
へ暗黙の型変換が可能になります。
引数をAny?にする
結論に書いたように、引数をAny?
とするのが正解です。
Optional
の暗黙的型変換、Optional
におけるas
の振る舞いの両者を踏まえ、a
は思い通りに動くようになりました。めでたしめでたし。
func a(value: Any?) -> String? {
if let string = value as? String {
return string
}
return nil
}
勿論これは、引数がAny
ではなくGenericsでも有効です。
Genericsの型推論が働く前に、Optionalの暗黙の型変換が働きます。
TODO
図は後で差し込むかもしれません。Optionalのキャストのあたり。