この記事ではSwiftのoptional value
の挙動と、利用することのメリット、具体的な利用シーンについてまとめてみました。
基本的な挙動について
まず?
と!
の挙動についてまとめてみます。 実際にどのようなケース利用するかの使い分けや、使うことのメリットについては次の章で説明します。
挙動の説明を見て、わかりづらいなと思ったら、この章は流し読みして次章の「具体的な利用シーン」とこの章を交互にみるとわかりやすいかもしれません。
optional valueな変数の宣言
Swiftでは、ただ宣言しただけの変数にはnil
を代入することができません。
例えば以下のようなコードは静的にエラーになります。
var optionalValue:String = "HelloSwift"
optionalValue = nil //この行で静的解析時にエラー
以下のように宣言の際に型の後ろに?
をつけることでnil
が代入可能な変数になります。
var optionalValue:String? = "HelloSwift"
optionalValue = nil // 正常に代入できる
!と?を利用したOptionalChain
var optionalString:String? = "HelloSwift"
println(optionalValue) // この行で静的解析時にエラー
optionalValueな変数の末尾には?
か!
の、いずれかを必ずつける必要があります。
2つの記号は変数にnil
が入っている場合に挙動が変わります。
!
を付けた場合は変数の中身がnil
だと実行時にエラーになります。
?
を付けた場合は変数の中身がnil
だとエラーにはなりません。
// 変数がnilの場合は、?か!で挙動が違う
var hoge:String? // 初期値はnil
println(hoge?) // hogeがnilなのでエラーにはならない
println(hoge!) // hogeがnilなので実行時エラー
// 変数に中身が入っている場合は?でも!でも挙動は同じ
var fuga:String? = "FugaFuga"
println(fuga?) // "FugaFuga"
println(fuga!) // "FugaFuga"
エラーが起きるのは静的解析時ではなく実行時であることに注意してください。
なぜなら変数の中身は実行時のコンテキストによって変わるからです。
またnil
に対してmethod
が呼ばれた場合結果はnil
となります。
optional valueに対してmethod
を利用してみましょう。考え方は先ほどと同じです。
// 変数がnilの場合は、?か!で挙動が違う
var hoge:String? // 初期値はnil
println(hoge?.uppercaseString) // hogeがnilなのでmethodの返り値もnil
println(hoge!.uppercaseString) // hogeがnilなので実行時エラー
// 変数に中身が入っている場合は?でも!でも挙動は同じ
var fuga:String? = "HogeHoge"
println(fuga?.uppercaseString) //FUGAFUGA
println(fuga!.uppercaseString) //FUGAFUGA
MethodをChainで重ねて書いた場合も、上記と考え方は同じです。
// とあるオブジェクトuserがoptional valueで
// さらにuserが所持しているプロパティprofileもoptional valueの場合のコード
var user:User?
// 以下はuserがnil、もしくはprofileがnilなら実行結果はnil
user?.profile?.sex
//以下はuserがnilなら実行時エラー /profileがnilなら結果がnil
user!.profile?.sex
//以下はuserもprofileも値が入っていなければ実行時エラー
user!.profile!.sex
何が便利で嬉しいのか?具体的な利用シーンから考える
状況の想定
具体的な状況を想像してみましょう。
このQiitaのようなサービス中のコードで1つの記事を表現するArticle
というクラスが存在し、以下のようなpropertyを持っているとしましょう。
Article#id
- 記事を識別するための文字列で必ず存在する仕様
Article#body
- 記事の本文が入る文字列で必ず存在する仕様
-
Article#category
: - 記事のカテゴリを表す文字列でオプショナル
これをSwiftのクラスで書くと以下のようなコードになるでしょう。
// 投稿記事を表現するArticleクラス
class Article {
var id : String
var body : String
var category : String? // optional value
init(id: String, body: String, category :String?){
self.id = id
self.body = body
self.category = category
}
}
さらに、記事(Article)のidを指定するとDBからArticleのインスタンスを取得する関数getArticleById
が存在していると想定します。
とりあえず実行できるように仮で中身を書いておきますが、呼び出すときは脳内でDBから取得してるような気分で読んでみてください
func getArticleById(id:Int) -> Article?{
// 本来はここにDBから取得する処理が入りますが
// 暫定的にあるID(1234)の記事だけがDBに存在するかのように振る舞うようにしておきます
let existentId:Int = 1234 // このIDの記事しかDBに存在しないとする
// IDが1234の場合のみArticleのインスタンスを返し,それ以外はnilを返す
return id == existentId //三項演算子
? Article(
id: existentId,
body: "すごい本文",
category: "Swift"
)
: nil
}
上記のコードの利用者になる
突然ですが、あなたは上記のコードを書いた直後に頭を打って中身を忘れたとしましょう。
しかしインタフェースだけは覚えています。もしくはインタフェースも忘れたけど補完を駆使してあなたはがんばります。
( 実際に頭を打たなくても、そんなことはよくありますね。また他人が書いたコードを利用して同じような状況になることはもっとあるでしょう。 )
さっそく、あなたは記事を表示する画面の開発に入ります。
###?
が活きる場面
あなたはgetArticleById
の詳細を忘れ、nil
が帰ってくることがあるということを忘れたまま
以下のようなコードを書いたとしましょう。
var article:Article = getArticleById(1234) // 静的解析でエラー
すると、あなたの利用しているIDEに警告がでます。 警告を見てあなたは気づきます。
「getArticleById
の返り値はoptional value
だ。 」
そして思い出します。
「 そういえばgetArticleById
は無効なIDを指定した時にはnil
を返し、存在するIDを指定した時のみArticle
のインスンタスを返す仕様だった」と。
静的解析による警告のみで気づけたことに注目してください。 コードを一度も実行せずに潜在的な問題に気づくことができました。
もし使っている言語がJava
のようにNULL
にmessageを送った時にエラーを起こす言語であれば、存在しないIDの画面を開くまで気づかなかったかもしれません。
もしくはObjective-C
のようにnil
に透過的にmessageを送れる言語だった場合、存在しないIDの画面を画面を開くと、なぜか画面が崩れていて、なぜ崩れているか、すぐにわからないかもしれません。
実行することなく問題に素早く気づけたあなたは以下のようなコードを書きます。
if let article = getArticleById(1234) {
println(article.body)
} else {
// 本来はここに「存在しないIDです」という画面を描画
println("存在しないID")
}
tips1 if文にoptional valueに渡した時の真偽判定
getArticleById
の返り値はoptinal value
ですが上記のif let article = getArticleById(1234)
の後ろには?
も!
ついていません。
これはif
の条件にoptional value
を渡した時の特殊な書き方です。 if文の条件がoptional value
だった場合、値が入っている場合にtrue
、値がnil
の場合にfalse
になります。
例えばvar booleanValue Bool?
という変数があった場合、if booleanValue
という条件文はbooleanValue
の中身の場合は中身が入っているのでtrue
という判定になります。 booleanValue
がnil
の場合のみfalse
となります。
nil
もしくはfalse
のときにif文が偽を返すようにしたいのであれば明示的に!
を使ってif booleanValue!
のように書きます。( !
でoptional value
を通常の値に変換しているのです )
tips2 if文にoptional valueに渡した時のBlockの中身
またif文の中のブロックのarticle.body
のarticleの後ろにも?
も!
つけていませんが、エラーになりません。
なぜなら そのコードはif let article = getArticleById(1234)
というブロックにあるのでnil
ではないことが自明なため
そのブロックの中では強制的にoptional value
ではなく通常の値に変換されています。=optional value
がunwrapp
された )
それではコードに戻りましょう
さて、あなたは引き続きコードを書いていきます。
そうだ、記事を描画するサブルーチンlayoutArticle
を作ろう、と思いつきます。
あなたは、またもや詳細な仕様を忘れarticle#category
がオプショナルな仕様であることを忘れてしまっているとしましょう。
さっそく以下のような定義をしてみます。
func layoutArticle(body:String , category:String) {
/* 記事を描画するクールに処理 */
}
できたので先ほどのif文の中で呼び出してみます。
layoutArticle( article.body, article.category) // 静的解析でエラー
エラーが起きてしまいました。
そして、あなたは思い出します。 article#categoryはオプショナルな仕様であって、値がはいってないことがあると。
今回も コードを実行する前 に 静的な解析で 忘れていた仕様を思い出すことができました。
以下のようにlayoutArticle
を修正します。
// 引数categoryをoptional valueに修正
func layoutArticle(body:String , category:String?) {
/* categoryが存在する場合としない場合で分岐させる */
}
!
を使う場面
例えばarticle#category
を表示する際の仕様として、カテゴリが入力されていない場合はなし
と出す仕様にするとします。
さらにlayoutArticle
を呼ぶ時点で、カテゴリがある場合はカテゴリ名が、ないばあいはなし
という文字列が既にはいっているとします。
つまりlayoutArticle
からすると引数で受け取る時点でcategoryはnil
ではないことが確定しています。
なので以下のように定義を戻します。
func layoutArticle(body:String , category:String) {
/* categoryは存在する前提で処理 */
}
一方layoutArticleを呼ぶ側のコードは以下の様に修正します。
if let article = getArticleById(1234) {
// categoryがnilの場合は明示的に"なし"を入れる
article.category = article.category ?? "なし"
// article.categoryは必ず入っているので!をつけてoptional valueから通常値にunwrapする
layoutArticle( article.body, article.category!)
}
??
演算子はOptional
の中身がnil
の時のみオプショナルでラップされた右の値が代入されます。
なので上の例ではcategoryの中身がnil
の場合はOptional("なし")
、値が入っていた場合は何も変化しません。
ちょっと、強引なコードになってしまいましたね。
イメージは伝わったでしょうか?
ただ上記の例は!
の活用というよりは、消極的に使ってる感じですね。
!
があるおかげで助かったと言えるような場面も考えてみましょう。
!
が活きる場面
例えばModel層に近いようなところで、 Articleのcategory
が更新されたことをシステム全体に通知するpub/sub的な関数を作ったとします。
( 実際は、自分で作らずライブラリ等、利用するかもですが。仮定として…)
そして「カテゴリ編集時間」を管理しているDBがあり、そちらはcategoryの編集をobserveしています。
func notifyUpdateCategory(artice:Article){
// この関数を実行すると、システム全体に通知する
// 例えばDB上に「カテゴリ編集時間」を更新する処理がある
}
設計する際に、あなたはArticle#category
がオプショナルなのは知っているがnil
かどうかチェックするのは、この関数の責務ではなく呼ぶ側がチェックすべきだと考えたとします。
func notifyUpdateCategory(artice:Article){
// この関数を実行すると、システム全体に通知する
// 例えばDB上に「カテゴリ編集時間」を更新する処理がある
_doNotify(NotifyTarget.Global, article.category)
}
上のコードを書いた時点で静的解析でエラーになります。
article.category
はoptional value
であるため末尾に?
か!
かのいずれかを指定しなければいけません。
あなたはobserver
達が値が存在することを前提としていること、副作用がある可能性があることを知っているので!
をつけることを選びます。
ここで ?か!のどちらかを選ばなければコンパイル、実行すらできなかった ことに注目してください。
もし、このnotifyUpdateCategory
を利用する人が、categoryのnil
チェックを自分の責務と思わずに、nil
のまま渡してしまったらどうなるでしょうか?
このコードは!
が入っているため, 実行時にcategoryがnil
だった場合、その時点で実行時エラーとなり、observer達がされることはありません。
もしoptional value
が存在しない言語であれば、関数の提供側、利用側、どちらもがnil
チェックは自分の責務ではないと思い込み、何らかの障害に繋がるかもしれません。
他の言語の例で考えてみます。
nil
にメッセージを送ってもエラーにならないObjective-C
の場合、本来実行されてはマズイobserver
が実行されてしまう可能性があります。
NULL
のメッセージを送るとエラーになるJavaの場合は実行すればエラーになっていたので問題はなかったかもしれません。
ただコードを書いている時点では、そのことを意識できていませんでした。
Swiftではnil
に対して!
でエラーにするのか?
で通すのか 意思決定しなければコンパイルが通らない のです。
まとめ
Swiftでは言語自体にoptional value
という概念があることで、開発者自身がコードを実行する前にnil
の状態を強制的に意識させられます。
これによりnil
が存在することで起きがちだった凡ミス、コミュニケーションミスなどを防ぐ、もしくは素早く気づくことができるのではないでしょうか。
(おまけ)Groovyのsafe navigationとの比較
Groovy
がAndoridで開発可能になったことから、Groovy
とSwift
との比較記事が出ています。
その中でGroovy
のsafe navigation
と、Swift
のoptional value
を同機能のように紹介しているものがあったのが気になったので、追記しておきます。
Groovy
(やJava)ではNULL
に対してメソッドを実行しようとすると実行時にエラーとなります。
そのため、例えばuser.profile.sex
のような構造があった時、user
やprofile
にnull
が入っている可能性があるときにsex
をとりだそうとすると愚直に書くと以下のようになります。
if (user != null && user.profile != null ) {
println user.profile.sex
}
groovy
のsafe navigation
変数の後に?
があった場合、その前の値がnull
であればメソッドを実行せずnull
を返します。
これを利用すると最初のコードを以下のようにスマートに書くことが出来ます。
println user?.profile?.sex
このようにGroovy
のsafe navigation
はコードを堅牢にするという意味よりは、nullチェックをより、スマートに書くための記法という意味合いが強いのではないでしょうか。
ちなみにObjective-C
では、nil
に対してmethod
を叩くとnil
が返ってくる仕様でした。
そのため以下のGoorvyのコードと、Objective-Cのコードは同じような挙動になります
// Groovy
user?.profile?.sex // ?でnull でもエラーにはならない
// Objective-C
[[user profile] sex] // objCでは元々nilにmessageを送ってもエラーにならない
そしてGroovyの?
を付けない場合の挙動はSwift
の!
をつけた場合の挙動と同じになります。
// Groovy
user.profile.sex // userかprofileがnullだった場合エラーになる
// Swift
user!.profile!.sex // optional valueに!をつけた場合のみnil値がエラーになる