Posted at

SwiftのAnyにOptionalを突っ込むと取り出せなくなる話

More than 3 years have passed since last update.

なんとなくHackerTackleで喋った内容について思い出したので、共有します。

端的に述べると、引数の型がAnyの場合にOptionalな値を突っ込むと死ぬよって話です。


結論

Any?を使いましょう


まずはじめに

任意の型からStringを取り出す関数aを考えます。

func a(value: Any) -> String? {

if let string = value as? String {
return string
}
return nil
}

valueStringならそれを返す。違えば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の有無による振る舞いはいつ決まるのでしょうか?

答えはコンパイル時です。従って、AnyOptionalが隠蔽されていることを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は、スーパーセットであるBas無しで型変換が可能です。

特にOptionalに関しては幾つかの暗黙の型変換があるので、覚えておくと役に立ちます。

1. A -> B => A -> B?

2. A? -> B => A -> B
3. A? => B? (A is a B)

1と2は、クロージャの型の一部がOptionalへ変換されるということです。これについてはまた別の機会に。

重要なのは3で、AOptionalは、スーパーセットであるBOptionalへ暗黙の型変換が可能なのです。

さて、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のキャストのあたり。