Swift

【Swift入門】オプショナル型を理解しよう

swiftの大きな特徴といえばOptional型ですね。
ですが初心者にとっては、Optional型というと、

  • Optionalってnilが入るやつ
  • ?!を使う
  • !を使ったときにnilが入るとアプリがクラッシュしてやばい
  • Xcodeに言われるがままにを多用する

こんな感じなふわふわした認識の人が多いかと思います。
今回そんな人のために、ふわふわした認識からちゃんと理解して安全に取り扱えるレベルまでなってもらえるようにこの記事を書いていこうと思います!

Optional型とは

Optional型とは通常の値とは別に「扱うデータがない」という状態をとれる型です。他の言語ではString型Int型といった通常の型にNULLという値が入ると思いますがSwiftでは通常の型にはNULLを入れることができず、もしNULLを扱いたい場合はOptional型という型を使わないといけません。Optional型がない言語では、プログラマーが想定していないところでNULLが入った場合に予期せぬ挙動が気づかないところで起こってたりする場合もありますが、Optional型があることで文法的にエラーを返してくれるので、文法通りに書けばバグをある程度回避できるメリットがあります。

といってもイメージがわかないと思うのでまとめると、

  • Optional型にするとnilが使えるようになる。
  • Optional型!を使って表現する。

Optional型の使い方

初心者の人にとってはOptional型を使うぐらいしかわかってない人が多いと思います。というのも使い所によって役割が違うからです。なので役割ごとに分けて説明します。

説明するにあたってまず分けて考えないといけないのが、に関してはOptional型以外にも使われる部分があるので先に整理しておきましょう

真偽値として使われる「!」

Optional型以外に!が用いられるのが真偽値です。if文とかで使われるやつですね。


if a != b {
    print("a")
}

これとか


let a = true
let b = !a //false

主にこの2種類があり、両方とも否定を表します。

  • 「!=」
  • 定数名や変数名の先にがつく

これらの場合は、Optional型でのではなく真偽値としてのというのに注意しましょう。

以上を踏まえた上でOptional型 の使い所の分類をしていきます。大きく分けて2つあります。

  • 型宣言で使う
  • それ以外のところで使う

この2種類になります。

型宣言で使う

Swiftでは変数や定数を定義する際に型も宣言しなければいけません。


let a: Int = 10
var b: String = "bbb"
let c = "ccc" //型推論で自動的にString型と認識してくれる

というように型を定義しますが、上の例のcみたいに代入する値から型が明らかな場合は型推論という機能がありswiftが型を推測してcString型と認識してくれます。本来は値から方が明らかな場合は宣言時に型を書かないほうがいいというセオリーがありますが今回はわかりやすいように毎回型を宣言するようにします。

Int型String型というように様々な型がありますが全ての型にをつけることでOptional型にすることができます。
例えばInt?とすればオプショナルInt型と呼び、Int型の値とnilを代入することができます。


let a: Int? = nil //初期値にnilを入れることが出来る

var b: Int? = 2 //nilじゃない値も入れることが出来る
b = nil //nilを代入することも出来る

var c: String? //宣言時に初期値を定義しなければ自動的にnilが入る
var d: String //nilを入れれないのでエラー

まとめ

型宣言時に (型名)? とすることによって、オプショナル(型名)型となってその型の値に加えてnilも代入できるようになる。

そういえば使ってなくない?って思ってる人もいるかもしれませんが、Int!というように書いた場合に関してもオプショナルInt型になりますが、特殊な書き方になるのでまたあとで説明します。

それ以外で使う

型宣言で使う場合は自分で定義するのでわりとわかりやすいですが、それ以外の場合で使うのは主に計算する場合です。

まず以下の例を見てみましょう。


let a: Int = 1
let b: Int? = 2
let c = a + b //コンパイルエラー

このようにエラーを起こしてしまいます。これはどうしてでしょうか?


let a: Int = 1
print(a) // 1

let b: Int? = 2
print(b) // Optional(2)

abprintしてみたところbのほうはOptionalというものがついています。
Optional型では本来の値をOptionalのラップが包んでいて本来の値としての計算ができません。
本来の型の値に加えてnilを代入することができるようになる代わりに、そのままでは計算などに使うことができなくなってしまいます。

もうひとつ例を用意しました。


let a: [String] = ["aa","bb","cc","dd"]
let a1 = a[0] // "aa"
let a2 = a // "bb"
let a3 = a // "cc"
let a4 = a // "dd"

aString型の配列でこのようにアクセスすることができます。
それではこの場合どうでしょうか?


let b: [String]? = ["aa","bb","cc","dd"]
let b1 = b[0] //コンパイルエラー
print(b) // Optional(["aa","bb","cc","dd"])

bString型の配列のOptional型です。aのときはa[0]というようにすれば配列の中の要素にアクセスすることができましたが、この場合エラーになってしまいます。これもOptionalのラップが配列全体を包んでいるので中身にアクセスすることができません。

おまけにですがこの場合はどうでしょう?


let c: [String?] = ["aa","bb",nil,"dd"]
let c1 = c[0] // Optional("aa")
let c2 = c // Optional("bb")
let c3 = c // nil
let c4 = c // Optional("dd")

cはオプショナルString型の配列で、配列全体が包まれているわけではなく、配列の要素1つ1つがラップに包まれているだけなのでc[0]というようにしてもアクセスができます。

なんとなくイメージはわいてきましたか?

ここまでの説明だと、まだOptional型の値を使うことができません。
では値を使えるようにするにはどうすればいいでしょうか。それは包まれてるラップを剥がしてあげればいいのです。

アンラップ

Optionalのラップを剥がすことをアンラップするといいます。
ラップをない状態にするのでunラップですね。

まず最初に一番簡単なアンラップの方法です。
ここでひさしぶりにがでてきます。


let a: Int? = 1
print(a) // Optional(1)
print(a!) // 1

aの後にをつけることによってOptionalのラップがはずれています。
このように変数名、定数名の最後にをつけることでアンラップすることができます。
しかしこの方法には致命的なデメリットがあって、


let a: Int? = nil
let b = a! //アプリがクラッシュする

このようにnilがはいったものをを使ってアンラップするとアプリがクラッシュします。
本来この方法はその変数にどんな値が入っていようが強制的にアンラップするのでForced Unwrapといいます。その変数にnilが入っていないという保証がある場合に使います。
一番簡単な方法とか言って紹介しておきながら、僕らの開発チームではこのアンラップの方法は特別な場合を除いて絶対に使わない決まりになっています。思わぬところでnilが入ってしまった瞬間にアプリがクラッシュしてしまうからです。

とはいえ、nilが入らないようにいいかんじに分岐すればいいんじゃね?ってプログラミングしたことがある人なら最初に思いつくと思うのがif文で分岐させる方法です。


let a: Int? = 1
if a != nil {
    let b = a!
    print(b) // 1
}

let c: Int? = nil
if c != nil {
    let d = c!
    print(d) // 通らない
} else {
    print("nil") // "nil"
}

というようにif文で分岐させれれば、アンラップ処理ができるようになるので良さそうに見えますが、こういった分岐が複雑になっていったときに思わぬところでnilが入ってしまいアプリがクラッシュする要因になります。もう一度いいますがForced Unwrapは危険です。
ということでこれと似た構文でアンラップ処理ができるものがswiftでは用意されています。それがif let構文です。

if let 構文

上の例で言うと


let a: Int? = 1
if let b = a {
    print(b) // 1
}

let c: Int? = nil
if let d = c {
    print(d) // 通らない
} else {
    print("nil") // "nil"
}

すこしスッキリ書くことができ、をつかわずに処理をすることができます。
ポイントとしては上のcを例に取ってみると

  • cnilじゃないとき、アンラップした値がdに入りif文の中に入る
  • dif文の中で使える
  • cnilの場合else文の中に入る
  • aの例のようにelse文を省略もできる

となります。こうすることで安全にアンラップ処理を行うことができます。
普通にコードを書く文にはif let構文がわかればなんとかなりますが、次のようなデメリットもあります。

  • アンラップ処理はif文の中にしかかけないので絶対にネストする
  • 複数のアンラップ処理があった場合にさらにネストが深くなる
  • if文の外に出るとアンラップしたものは使えない
    これらようなデメリットが有り、見にくいコードになりやすくなってしまいます。

それを改善するのがguard let構文です。

guard let 構文


let a: Int? = 1
guard let b = a else { return }
print(b) // 1

let c: Int? = nil
guard let d = c else { return } // return される
print(d) 

このようにさらにスッキリ書くことができます。

if let構文に比べて

  • ネストが深くならない
  • アンラップしたものをずっと使うことが出来る

といったメリットがありますが、

  • nilの場合は処理を終了させてしまう前提なのでそこで処理が終わってしまいます(書き方次第ではありますが)

といったデメリットもありますが、これに関しては使い方次第です。nilの場合は何もしない場合とかはとくに便利です。

ここで少し話を戻すと、アンラップ処理をif文から順を追って説明しましたが、if文を使う際に三項演算子を用いてスッキリ書くテクニックがありますね。それを今回のアンラップの話に置き換えてみると、


let a: Int? = 1

let b = a != nil ? a! : 0
print(b) // 1

let c: Int? = nil
let d = c != nil ? c! : 0
print(d) // 0

このように書くことが出来ると思います。
おさらいですがここででてくる!=は否定の意味ですよ!
三項演算子でbにはa != niltrueだった場合に aを代入し、falseだった場合に、0を代入します。
スッキリ書くことが出来るようになりましたがこれもが使われててクラッシュする危険性を残しています。

これもswiftでは構文が用意されています。

nil合体演算子

上の例で言うと


let a: Int? = 1
let b = a ?? 0
print(b) // 1

let c: Int? = nil
let d = c ?? 0
print(d) // 0

このように、


(nilじゃないときにアンラップして代入する値) ?? (nilのときに代入する値)

というように分けます。
こうすることで、スッキリ書くことができ、も使わないで書くことができます。

僕らのチームではif let構文は殆ど使わずに、

  • nilのときに処理を終わらせたいときにguard let
  • nilのときでも何かしらのそれっぽい値を入れて処理を続けたいときはnil合体演算子

この2つを使い分けています。

ここまで簡単な値を例に取ってきましたが、途中ででてきて放置してたこの例をもっかい引っ張り出してきましょう。


let b: [String]? = ["aa","bb","cc","dd"]
let b1 = b[0] //コンパイルエラー
print(b) // Optional(["aa","bb","cc","dd"])

これは配列全体がOptionalのラップに包まれているので配列の要素にアクセスできないという例でした。
上で書いたアンラップの方法を使ってアクセスできる配列の中身を取り出せるようにしていきたいと思います。


let b: [String]? = ["aa","bb","cc","dd"]
guard let c = b else { return }
print(c) // ["aa","bb","cc","dd"]
let c1 = c[0] // "aa"
let c2 = c // "bb"
let c3 = c // "cc"
let c4 = c // "dd"

このようにすることで配列の要素にアクセスすることができました。
ここで次の例を考えてみましょう


let a: [String]? = ["aa","bb","cc","dd"]
let countA = a.count // 配列の名前に.countをつけることで配列の要素の数を返してくれる
print(countA) // 4

let b: [String]? = ["aa","bb","cc","dd"]
let countB = b.count // コンパイルエラー

swiftでは配列に対してcountというメソッドが用意されていて、配列の要素の数を返してくれます。しかしbの場合は配列全体がOptionalのラップに包まれているのでcountというメソッドを使うことができません。
おそらくこのコードを書いたときにXcode


let countB = b!.count

と書きなさいと言われると思います。初心者の人だとよくわからずそれに従ってしまうわけですね。
Optional型の配列だったbがをつけることによってアンラップされて普通の配列になるのでcountが使えるようになりますが、もしbnilだった場合にアプリがクラッシュするので危険です。
これに対しても先程配列の要素にアクセスするようにしたように、


let b: [String]? = ["aa","bb","cc","dd"]
guard let c = b else { return }
let countC = c.count // 4

というようにすればちゃんと適切な値が返るようになりました。このままでも十分な気がしますが、これもさらに簡単に書くことができます。

Optional Chaining

Optional型の場合に上の例みたいにメソッドを使ったり配列内の要素にアクセスしたいときはアンラップしてから使うことになり毎回アンラップ作業が必要になります。これを解決するのがOptional Chainingです。
先程の、


let countB = b!.count

ではbnilの場合にクラッシュしてしまいますが、これを以下のように書くことができます。


let b: [String]? = ["aa","bb","cc","dd"]
let countB = b?.count
print(countB) // Optional(4)

こうすることでguard letをはさむことなくcountを使うことができました。しかしよく見てみるとcountBOptinal型になっています。
これはなぜかというとbnilではない場合にはbをアンラップしてから配列の要素を数えてくれて4が返ってきますが、bnilの場合はcountは計算せずにnilを返してくれます。bの値によってcountBに入る値がnilも含んでいるのでcountBOptional型になります。
countBOptional型にしたくない場合はどうすればよいでしょうか?


let b: [String]? = ["aa","bb","cc","dd"]
let countB = b?.count ?? 0
print(countB) // 4

先程のnil合体演算子を使えばbnilの場合に0が入ってくれるのでcountBOptional型ではなくなります。今の話は、型推論の知識も少し必要になるのでもしわからなければ型推論を学ぶと良いと思います。
このコードだと?がいっぱい出てきてて、しかも全て役割が違いますが、ここまで読んでいればそれぞれの意味が全てわかるようになったかと思います。

これだけだとOptional Chainingのメリットがあまり感じられないかもしれませんが、軽く例をあげてみると、


self?.tableView?.reloadData()

このように2つ?が続いた場合にif letguard letでアンラップした場合に処理が多くなってしまいますが、この書き方で書いた場合はselftableViewnilじゃないときに全体が実行されます。逆に言うとどちらかがnilの場合はなにも起こりません。今回は2つの場合でしたがこれが何個でも続く場合もこのように書くことができます。

  • 何個でも?で続けることが出来る
  • どこでnilになってもないも起きないだけなので安全

このようなメリットがあります。

さて、だいぶ長くなってしまいましたが以上でオプショナルの説明は終わりです。最後にまとめると

? !
型宣言 Int? Int!(後日書きます)
アンラップ nil合体演算子 ?? Forced Unwrap(使わない)
Optional Chaining b?.count b!.count (使わない)

こんなかんじでした。