swift
の大きな特徴といえばOptional型
ですね。
ですが初心者にとっては、Optional型
というと、
Optional
ってnil
が入るやつ?
と!
を使う!
を使ったときにnil
が入るとアプリがクラッシュしてやばいXcode
に言われるがままに!
を多用する
こんな感じなふわふわした認識の人が多いかと思います。
今回そんな人のために、ふわふわした認識からちゃんと理解して安全に取り扱えるレベルまでなってもらえるようにこの記事を書いていこうと思います!
Contents
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
が型を推測してc
はString型
と認識してくれます。本来は値から方が明らかな場合は宣言時に型を書かないほうがいいというセオリーがありますが今回はわかりやすいように毎回型を宣言するようにします。
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)
a
とb
をprint
してみたところ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"
a
はString型
の配列でこのようにアクセスすることができます。
それではこの場合どうでしょうか?
let b: [String]? = ["aa","bb","cc","dd"]
let b1 = b[0] //コンパイルエラー
print(b) // Optional(["aa","bb","cc","dd"])
b
はString型
の配列の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
を例に取ってみると
c
がnil
じゃないとき、アンラップした値がd
に入りif文
の中に入るd
はif文
の中で使えるc
がnil
の場合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 != nil
がtrue
だった場合に 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
が使えるようになりますが、もしb
がnil
だった場合にアプリがクラッシュするので危険です。
これに対しても先程配列の要素にアクセスするようにしたように、
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
ではb
がnil
の場合にクラッシュしてしまいますが、これを以下のように書くことができます。
let b: [String]? = ["aa","bb","cc","dd"]
let countB = b?.count
print(countB) // Optional(4)
こうすることでguard let
をはさむことなくcount
を使うことができました。しかしよく見てみるとcountB
はOptinal型
になっています。
これはなぜかというとb
がnil
ではない場合にはb
をアンラップしてから配列の要素を数えてくれて4
が返ってきますが、b
がnil
の場合はcount
は計算せずにnil
を返してくれます。b
の値によってcountB
に入る値がnil
も含んでいるのでcountB
はOptional型
になります。
countB
がOptional型
にしたくない場合はどうすればよいでしょうか?
let b: [String]? = ["aa","bb","cc","dd"]
let countB = b?.count ?? 0
print(countB) // 4
先程のnil合体演算子
を使えばb
がnil
の場合に0
が入ってくれるのでcountB
はOptional型
ではなくなります。今の話は、型推論の知識も少し必要になるのでもしわからなければ型推論を学ぶと良いと思います。
このコードだと?
がいっぱい出てきてて、しかも全て役割が違いますが、ここまで読んでいればそれぞれの意味が全てわかるようになったかと思います。
これだけだとOptional Chaining
のメリットがあまり感じられないかもしれませんが、軽く例をあげてみると、
self?.tableView?.reloadData()
このように2つ?
が続いた場合にif let
やguard let
でアンラップした場合に処理が多くなってしまいますが、この書き方で書いた場合はself
もtableView
もnil
じゃないときに全体が実行されます。逆に言うとどちらかがnil
の場合はなにも起こりません。今回は2つの場合でしたがこれが何個でも続く場合もこのように書くことができます。
- 何個でも
?
で続けることが出来る - どこで
nil
になってもないも起きないだけなので安全
このようなメリットがあります。
さて、だいぶ長くなってしまいましたが以上でオプショナルの説明は終わりです。最後にまとめると
? | ! | |
---|---|---|
型宣言 | Int? | Int!(後日書きます) |
アンラップ | nil合体演算子 ?? | Forced Unwrap(使わない) |
Optional Chaining | b?.count | b!.count (使わない) |
こんなかんじでした。