[Swift][Optional] アンラップしているはずなのに、アンラップされない

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

この記事について

この記事は、以前書いた「[Swift] Optional型についてのまとめ」の補足記事です。

開発環境

  • Xcode 6 Beta6 (Playground)
  • Mac OS X 10.9.3

Optional Chaining でアンラップしているはずなのに、アンラップされない

以下のコードを見てください。

Optional Chaining(? を使ってアンラップする方法)でアンラップしているはずなのに、アンラップされていないように見えます。

class Dog {}

var wrapped: Dog?  = Dog() // Optional 型
var unwrapped: Dog = Dog() // 非 Optional 型

//
// 非 Optional 型に Optional 型は代入できない
//
unwrapped = wrapped // -> error: value of optional type 'Dog?' not unwrapped...

//
// Forced Unwrapping でアンラップすると、正常に代入できる
//
unwrapped = wrapped!

//
// しかし、Optional Chaining でアンラップすると、代入できない
//
unwrapped = wrapped? // -> error: value of optional type 'Dog?' not unwrapped...

Optional 型の変数を、非 Optional 型の変数に代入する際の挙動が、Forced Unwrapping(! を使う方法)と Optional Chaining(? を使う方法)で異なっています。

  • Forced Unwrapping(! を使う方法)でアンラップする場合 -> 正常に代入できる

  • Optional Chaining(? を使う方法)でアンラップする場合 -> エラーが出る(アンラップしていない、というエラーが出る)

Optional Chaining でアンラップしているはずなのに、「アンラップされていない」というエラーが出てしまいます。なぜでしょうか?

これは、

  • アンラップした際の戻り値が、Forced Unwrapping と Optional Chaining で異なる

ためです。以降、詳しく見ていきます。

Optional Chaining は、戻り値をラップしてから返す

筆者は、Optional Chaining について、

  • 一度アンラップしてメソッド等を呼び出したあとに、戻り値を再度ラップし直してから、返している

というイメージで捉えています。

以下の Dog クラスを例に、具体的に見ていきます。

class Dog {
    func bark() -> String {
        return "Wan!"
    }
}

var wrapped: Dog? = Dog() // Optional 型

Forced Unwrapping(! を使う方法)の場合、戻り値は、素の型

アンラップ時の内部処理のイメージ その1

println(wrapped!) // -> __lldb_expr_11.Dog(= Dog 型)

上記のコードのアンラップ時の内部処理は、以下のようになっているものと推測されます。

  1. wrapped をアンラップする
  2. アンラップした値を「そのまま」返す

アンラップ時の内部処理のイメージ その2

println(wrapped!.bark()) // -> Wan!(= 非 Optionnal の String 型)
  1. wrapped をアンラップする
  2. bark() を呼び出す
  3. bark() の戻り値(非 Optional の String 型)を「そのまま」返す

Optional Chaining(? を使う方法)の場合、戻り値は、Optional 型

アンラップ時の内部処理のイメージ その1

println(wrapped?) // -> Optional(__lldb_expr_11.Dog) (= Optional の Dog 型)
  1. wrapped をアンラップする
  2. 再度、wrapped を「ラップし直してから」返す

アンラップ時の内部処理のイメージ その2

println(wrapped?.bark()) // -> Optional("Wan!") (= Optional の String 型)
  1. wrapped をアンラップする
  2. bark() を呼び出す
  3. bark() の戻り値(非 Optional の String 型)を「ラップしてから」返す。

上記のように、Optional Chaining の場合は、最終的な戻り値を Optional 型に「ラップしてから」返しています。

これは、Optional Chaining が、nil を返す可能性があるためです(詳細は、以前書いた記事を参照してください)。nil を許容するためには、戻り値は Optional 型でなければいけません。

補足(元々の戻り値が Optional 型だった場合、戻り値は Optional 型のまま)

元々の戻り値が Optional 型だった場合は、Optional Chaining の戻り値も、Optional 型のままになります。さらに、ラップするようなことはしません。

アンラップ時の内部処理のイメージ

//
// Dog 型に wrappedBark() を追加
//
extension Dog {
    func wrappedBark() -> String? { // Optional の String 型を返す
        return "Wrapped Wan!"
    }
}

var wrapped: Dog? = Dog()       // Optional 型
println(wrapped?.wrappedBark()) // -> Optional("Wrapped Wan!") (= Optional の String 型)
  1. wrapped をアンラップする
  2. wrappedBark() を呼び出す
  3. wrappedBark() の戻り値(Optional の String 型)を「ラップせずに、そのまま」返す。

注)
Optional 型をさらに Optional 型でラップした場合は、以下のような出力になります。

var i: Int?? = 1 // Optional 型の Optional 型
println(i)       // -> Optional(Optional(1)) 

Optional Chaining と Optional Binding を組み合わせた場合

アンラップ時の内部処理のイメージ

if var unwrapped = wrapped?.bark() {
    println(unwrapped) // -> Wan! (= 非 Optional の String 型)
}

[Optional Chaining] 1. wrapped をアンラップする
[Optional Chaining] 2. bark() を呼び出す
[Optional Chaining] 3. bark() の戻り値(非 Optional の String 型)を「ラップしてから」返す。
[Optional Binding ] 4. 上記 3. の値をアンラップしてから、unwrapped に代入する

Forced Unwrapping と Optional Binding を組み合わせた場合

アンラップ時の内部処理のイメージ

if var unwrapped = wrapped!.bark() { // -> error: bound value in a conditional binding must be of Optional type
}

Optional Binding では、代入する側の値(ここでは、wrapped!.bark())は Optional 型でなければなりません。上記のコードでは、wrapped!.bark() の戻り値が、非 Optional 型であるため、エラーが発生します。

[Forced Unwrapping] 1. wrapped をアンラップする
[Forced Unwrapping] 2. bark() を呼び出す
[Forced Unwrapping] 3. bark() の戻り値(非 Optional の String 型)を「そのまま」返す
[Optional Chaining] 4. 上記 3. の値をアンラップすることができないため、エラーが発生する

Implicitly Unwrapped Optional 型の場合、戻り値は、素の値

Implicitly Unwrapped Optional 型についても、見ていきます。

var iwrapped: Dog! = Dog() // Implicitly Unwrapped Optional 型

アンラップ時の内部処理のイメージ その1

println(iwrapped) // -> __lldb_expr_11.Dog (= Dog 型)
  1. iwrapped を「暗黙的に」アンラップする
  2. アンラップした値を「そのまま」返す

アンラップ時の内部処理のイメージ その2

println(iwrapped.bark()) // -> Wan! (= 非 Optional の String 型)
  1. iwrapped を「暗黙的に」アンラップする
  2. bark() を呼び出す
  3. bark() の戻り値(非 Optional の String 型)を「そのまま」返す

注意

上記の一連の内部処理のイメージは、筆者の推測によるものです。コンパイラの実装がどうなっているのかは不明です。

ただ、上記のように捉えておけば、アンラップ時の戻り値について、理解しやすいのではないかと思います。

まとめ

他のアンラップ方法と異なり、Optional Chaining (? を使う方法)は、一度アンラップしたあとに、最終的な戻り値を Optional 型にラップしてから返します。

Optional Chaining を使う際は、注意してください。

アンラップの方法 戻り値
Forced Unwrapping wrapped!.bark() 素の型
Implicitly Unwrapped Optional 型 wrapped.bark() 素の型
Optional Chaining wrapped?.bark() Optional 型(最終的な戻り値を Optional 型にラップする)

参考文献

以下の 1. のブログ記事(「疑問」の項目)が、今回の記事を書くきっかけになりました。ありがとうございました。

  1. NTEC - 【Swift入門】Optional型 (?, !) をまとめてみた

  2. The Swift Programming Language