1. Qiita
  2. Items
  3. Swift

[Swift] Swiftのoptional valueの便利さ /「?」と「!」でより堅牢なコードへ

  • 419
    Like
  • 6
    Comment
More than 1 year has passed since last update.

この記事では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という判定になります。 booleanValuenilの場合のみ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 valueunwrappされた )

それではコードに戻りましょう

さて、あなたは引き続きコードを書いていきます。
そうだ、記事を描画するサブルーチンlayoutArticleを作ろう、と思いつきます。
あなたは、またもや詳細な仕様を忘れarticle#categoryがオプショナルな仕様であることを忘れてしまっているとしましょう。

さっそく以下のような定義をしてみます。

func layoutArticle(body:String , category:String) {
   /* 記事を描画するクールに処理 */
}

できたので先ほどのif文の中で呼び出してみます。

ts
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.categoryoptional 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で開発可能になったことから、GroovySwiftとの比較記事が出ています。
その中でGroovysafe navigationと、Swiftoptional valueを同機能のように紹介しているものがあったのが気になったので、追記しておきます。

Groovy(やJava)ではNULLに対してメソッドを実行しようとすると実行時にエラーとなります。
そのため、例えばuser.profile.sexのような構造があった時、userprofilenullが入っている可能性があるときにsexをとりだそうとすると愚直に書くと以下のようになります。

if (user != null && user.profile != null ) {
    println user.profile.sex
}

groovysafe navigation変数の後に?があった場合、その前の値がnullであればメソッドを実行せずnullを返します。
これを利用すると最初のコードを以下のようにスマートに書くことが出来ます。

println user?.profile?.sex

このようにGroovysafe 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値がエラーになる