【Swift】Optional型を安全にunwrapしよう

  • 55
    Like
  • 0
    Comment
More than 1 year has passed since last update.

これはなに

Swift の型は nil を許容しないので、許容するために Optional を使う場面があるでしょう。
Optional を使って、代入や参照の際に nil にアクセスしてランタイムエラーで落ちるのはもうやめましょう。
そのためにはどうしたらよいかという例を数点挙げてみようと思います。

実行環境は、 Xcode 6.1.1 の Playground です。

もくじ

  • Optional型とは
  • Forced Unwrapping
  • nil チェック
  • Nil Coalescing Operator
  • Optional Binding
  • Optional Chaining

Optional型とは

前述の通り、Swift の型は nil を許容しません。

var s: String = "hello world"   // "hello world"
s = "こんにちは"                 // "こんにちは"
s = nil                         // error: Type 'String' does not conform to protocol 'NilLiteralConvertible'

しかし、例外処理を nil 判定で行いたいときなど nil を使いたい場面は多いと思います。そんなときに Optional型を使います。

var s1: String? = "hello world" // Some("hello world")
s1 = "こんにちは"                // Some("こんにちは")
s1 = nil                        // nil

Optional<T> は、 Some(T)None の enum になっています。 T? は省略形なので、この文は以下のようにも初期化できます。

var opS: Optional<String> = "hello world"  // Some("hello world")

こう書けば、なんとなく StringString? は別の型であると感じられますね。

なので、引数に 型T を要求するメソッドに T? は入らないということです。

func upper(s: String) -> String {
    return s.uppercaseString
}

upper(opS)   // error: Value of optional type 'Optional<String>' not unwrapped; did you mean to use '!' or '?'?

ここでサジェストにしたがって、 upper(opS!) としてしまうならば Objective-C 時代となんら変わらない操作になってしまいます。
適切に nil を取り除いてやりましょう。

Forced Unwrapping

Optional の、 Optional を無理やり引っぺがす処理です。
そのため、Optional が nil であれば実行時に落ちます。

let str: String? = nil
str!.uppercaseString    // Execution was interrupted. reason: EXC_BAD_INSTRUCTION (code=EXC_i386_INVOP, subcode=0x0)

ちなみにこの ! は型についているものとは違うので注意してください。

let str: String! = "hoge"  // Forced Unwrapping ではない

これは、"Implicitly Unwrapped Optionals" といいます。まあ、暗黙的に Unwrap されているだけなので、nil が入ったオブジェクトを参照すると Forced Unwrapping と同じく実行時に落ちます。Forced Unwrapping は操作で、Implicitly Unwrapped Optionals は型ですね。

nil チェック

ある程度の規模のコードになると、Optional で与えられた変数には nil が入っている可能性があると考えます。もし nil でないことがわかっているならばいたずらに Optional を使うべきではありません。

まずは普通に nil チェックをしてみます。

let opS2: String? = "hello world"

if opS2 != nil {
    opS2 = opS2!.uppercaseString    // "HELLO WORLD"
}
// この if 文は以下のようにも書ける
if opS2 != .None {
    opS2 = opS2!.uppercaseString    // "HELLO WORLD"
}

Optional が enum だと知っていれば .None でも比較が可能ということがわかりますね。
しかしこの場合は .None で比較する必要がないので、別の例を出してみます。

func responseHander(response: NSURLResponse?, error: NSError?) {
    switch (response, error) {
    case (_, .Some):
        println("error")
    case (.Some, .None):
        println("error is nil")
    default:
        println("default")
    }
}

もちろん if文でも書くことはできますが、このようにエラーハンドリングすることもできます。
要素が多くなると switch文が膨れ上がってしまうので、その場合は別途条件判定文が必要になると思います。

Nil Coalescing Operator

参照したい値が nil の時はデフォルトの値を入れる、という操作の場合には Nil Coalescing Operator が便利です。

?? という演算子で、三項演算子の省略形です。

let str: String? = "hello world"
// Some("hello world")
let strNotNil: String = str != nil ? str! : ""   // 三項演算子
// "hello world"
let strNotNil2: String = str ?? ""               // Nil Coalescing Operator
// "hello world"

見やすいですね。

Optional Binding

if obj != nil で nil でないことを保証しても、後々参照するときに Forced Unwrapping するのは面倒なので、Optional Binding を使います。

let string: String? = "hello world"

if let bindString = string {    // Optional Binding
    bindString = bindString.uppercaseString
}

この if let ~ というのが Optional Binding です。
再代入可能変数にしたければ、if var ~ でもいいです。
右辺のオブジェクトが nil でなければ、unwrap された型の変数に格納され、あとはその変数を使うことができます。

ただ、一つネストが深くなるため、Optional を参照するときはいつも Binding とすると、ネストが深くなるだけのコードが生まれます。
なので、他の方法が使えないか考えた上で使いましょう。

Optional Chaining

Optional のメソッドやプロパティを参照しようとしたときに、nil かどうかチェックしてくれる機能です。
nil でなければ unwrap して参照をし、 nil ならばそこで評価をやめる(nil を返す)というものです。

class Cloud {
    var name: String?
}

let uroko = Cloud()
uroko.name?.uppercaseString   // name は nil なので、uppercaseString は実行されない

ここで、 uroko.name!.uppercaseString と Forced Unwrapping すると実行時に落ちます。
実践的には、Optional Binding を使うとよいと思います。

let uroko = Cloud()
uroko.name = "uroko"

if let urokoUppercase = uroko.name?.uppercaseString {
    println(urokoUppercase)                            // "UROKO"
}

まとめ

  • Forced Unwrapping を安易にせず、状況に合った方法で適切に nil を処理していきたいですね。

ちなみに、今回参考にしたのは以下のページです。