Swift Optionalの実際のアプリにおける使い方を考える
Swiftを学習していて、初心者が最初に嵌まるのがOptionalだと思う。分かってしまえば単純なのだが、ライブラリにおける使い方が途中で何度も変わって、ネット上のサンプルがうまくコンパイルできなかったり、コンパイラのエラー出力がOptionalが原因とすぐには分からないような表示だったりするせいで、余計に印象が悪くなってしまう。
自分も最初はかなり悩まされたが、アプリを1つ作ってみて大体使い方が分かってきたので、実際のアプリにおいてOptionalをどう使っていけばよいかをまとめてみた。
まずは仕様のおさらいから...
- 非Optional型
- nilを入れられない (nilになる可能性がない)。
- 普通に参照できる。
- Optional型
- nilを入れられる (nilになる可能性がある)。
- (明示的に) unwrapしないと中身を参照できない。
- Implicitly unwrapped optional型
- Optional型と同じだが、参照時に勝手に (暗黙的に) unwrapされる。
変数宣言時
class Hello {
func hello() { println("hello") }
}
var a: Hello // 非Optional型
var b: Hello? // Optional型
var c: Hello! // Implicitly unwrapped optional型
上記は通常の変数宣言の場合だが、関数の引数・戻り値に関しても同様の表記で宣言が可能。
変数参照時
非Optional型は普通に参照できる。ただし、未初期化の状態で参照するとコンパイルエラー (used before being initialized)。
Optional型は、何らかの方法でunwrapしないと参照できない。unwrapする方法は、以下の4通りがある。
b!.hello() // Forced unwrap
b?.hello() // Optional chaining
if var _b = b { _b.hello() } // Optional binding
if b != nil { ... } // 比較演算におけるunwrap
- Forced unwrap
- 強制的にunwrapする。
- 参照した変数がnilでなければ非Optional型扱いになるが、nilであれば実行時エラーになる。
- Optional chaining
- 参照する変数がクラスオブジェクトの場合のメソッド呼び出し・プロパティ参照、関数オブジェクトの場合の関数呼び出し時のみ使用可能。
- 参照する変数がnilでなければメソッド実行・プロパティ参照されるが、戻り値はOptional型に変換される (nilが返る可能性があるため)。nilであれば何もしないでnilが返る。
- Optional binding
- if/for/while文の条件式で宣言した変数にOptional型変数を代入する記述。
- 参照する変数がnilでなければunwrapされて代入され、ブロック内だけで参照可能。nilのときは代入されず、条件式がfalseになる。
- 比較演算子
- 比較演算実行時に一時的にunwrapされる。
- 論理演算 ("==", "!=") だけでなく算術演算 ("<", ">", ...) 等すべての比較演算子が対象。
- 参照した変数がnilでなければ通常通り演算される。nilであれば、nilとの比較以外のすべての比較演算がfalseとなる。
Implicitly unwrapped optional型は、上記4つの参照方法がOptional型と同様に可能。それに加えて、普通に参照すると (unwrapしないと参照できない状況では) 自動的にunwrapされる。
c.hello() // Implicitly unwrap
このとき、参照した変数がnilであれば実行時エラーになる。
実際の使い方
おさらいと言いつつある程度正確に書こうとすると長くなったが、これを踏まえて実際にアプリを作ってみた上で、個人的にこう使えばよいのではないかと思った使い方が以下。
初期化できるなら初期化する
Optional型を使うことになるのは宣言時に初期化できないときなので、初期化してしまえばそもそもOptionalを使う必要がない。初期化できないのは、その時点で入れるべき値が決まらず、かつその状態 (値が入っていないこと) を区別したいから。しかし、例えば文字列や配列の場合は、
var a: String = ""
var b: [Int] = []
のように空文字列、空配列で初期化してしまう手がある。空文字列・空配列を「未設定状態」と見なせるのであれば、このように初期化してしまえばOptionalを使わなくてすむ。
実際に使う時点でnilが入らないならImplicitly unwrapped optional型にする
これはクラスのプロパティの場合でよくあるが、宣言 (or イニシャライザ) 時点では初期化できなくてもviewDidLoad() 等で初期化していて、実際に参照する時点では必ず初期化済みというもの。このように、自分で挙動がよく分かっている場合はImplicitly unwrapped optionalにしてしまってもよいと思う。
もちろん、他人がコードを触ったり自分でも後で忘れた頃に触る場合に、うっかり初期化前に参照してしまう可能性はあるが、参照するたびにnilチェックや!を付けるのは可読性を下げるし、何も考えずに!を付けているのであれば、Implicitly unwrapと変わらないので。
挙動がよく分からないときはForced unwrapはなるべくしない
上記とは逆に、既存ライブラリのメソッドなどでOptional型が返されるときは、挙動がよく分かっていなければなるべくForced unwrapはしない方がよい。メソッドの呼び出しで戻り値も使わないのであれば、Optional chainingしておけばnilであれば実行されないだけなので便利 (ただし、実行されなければ結局自分の意図通りには動いていないわけで、逆に嵌まることも)。
変数の時はどこかでForced unwrapするしかないので、そのときはきちんとnilチェックしてからunwrapする。
Optional bindingでnil判定しつつ条件分岐
nil判定してnilでないときだけ何か処理をしたいときは、Optional bindingを使うのが常套手段。
if let _b = b {
// _b を使って正常時の処理
} else {
// エラー処理
}
しかし、微妙に使いにくいのが、nilのときとそれ以外のエラーで同じエラー処理をしたいとき。Optional bindingでunwrapした変数はブロック内でしか参照できないので、
if (let _b = b) && !b.error() {
...
}
のような書き方はできない (そもそも、Optional bindingの後に条件式をつなげること自体できないみたい)。このようなときは素直に、
if b == nil || b!.error() {
// エラー処理
} else {
// 正常時の処理
}
のように書いた方がよいと思う。ただし、ブロック内ではForced unwrapして参照する必要がある。
関数の引数に関数を渡すときに使う
これは、安全性を高めるというより便利なOptionalの使い方だが、コールバック関数のように関数の引数として渡して後で呼び出されるような場合に、コールバック不要なのでnilを渡したいときがある。そのような場合には、
func hoge(a: Int, b: String, callback: ((c: Int)->Void)!) {
...
callback?(123)
}
のようにOptional型で宣言して呼び出し時にOptional bindingしておけば、nilであれば実行しないようにできる。なお、宣言時の!は?でもよい。というか、?の方が安全な気もする。