LoginSignup
64
56

More than 5 years have passed since last update.

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

Posted at

なんとなく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のキャストのあたり。

64
56
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
64
56