Swift の初心者向けの解説を読んでいると、次のような説明を見かけることがあります。
Int?
のように?
を付けるとオプショナル型になります。オプショナル型の値を取り出すには!
を付けて強制アンラップします。
まちがってはいないのですが、 この説明では誤解を生みかねません 。オプショナル型が何のためにあるのかに立ち戻って、適切な使い方を説明します。
オプショナル型がないと何が起こる?
オプショナル型は何のためにあるのでしょうか。もし Swift にオプショナル型がなかったらどのような問題が起こるでしょうか。
例として、ユーザーに年齢を入力してもらい、成人か確認するコードを考えてみましょう。 18 歳以上の場合には 成人です。
と表示します。
年齢( age )が 18 歳以上かチェックするには if age >= 18
のような条件を書きます。しかし、テキストフィールドに入力された年齢は文字列なので String
型です。 age >= 18
のように比較するには、 age
が String
型ではなく Int
型である必要があります。
String
を Int
に変換するには↓のように書きます( text
は String
型とします)。
let age = Int(text)
Int(text)
が text
を Int
型に変換し、その結果が age
に代入されます。たとえば、テキストフィールドに "20"
という文字列が入力されていたら age
の値は整数の 20
になります。
しかし、もし "ABC"
という文字列が入力された場合はどうなるでしょうか。 "ABC"
を Int
(整数)に変換することはできません。そんな場合は、変換に失敗したことを表す nil
になります。
Swift では Int
型の変数や定数に nil
を代入することができません。 nil
を代入したい場合は ?
を付けて Int?
型とします。これがオプショナル型です。 Int?
型なら Int
型の値か nil
を、 String?
型なら String
型の値か nil
を代入することができます。↑のコードでは age
は Int?
型になります。
しかし、オプショナル型がない言語では、どんな型の変数にも nil
を代入することができます。 Int
型でも String
型でも、常に nil
が代入されているかもしれません。もしオプショナル型がなければ、↑のコードの age
は Int
型(ただし nil
かもしれない)ということになるでしょう。
このとき、成人かチェックして 成人です。
と表示するコードはどのように書けば良いでしょう。たとえば、↓のように書いたとします。
// オプショナル型が『ない』とき
let age: Int = Int(text)
if age >= 18 {
print("成人です。")
}
オプショナル型がないなら、↑のコードはコンパイルに成功し、実行することができるでしょう。 age
は Int
型なので、 age >= 18
のような比較は型の上では問題ありません。
しかし、テキストフィールドに "ABC"
のような文字列が入力されたらどうなるでしょうか。 age
は nil
なので、 nil >= 18
という比較をすることになります。 nil
と 18
の大小関係を比較することはできないので、実行時エラーになってプログラムはクラッシュしてしまいます(実際に、オプショナル型を持たない言語ではそのような挙動になります)。
"ABC"
のような文字列を入力された場合でもプログラムがクラッシュしないようにするには、 age >= 18
をする前に age
が nil
でないことをチェックしておきます。
// オプショナル型が『ない』とき
let age: Int = Int(text)
if age != nil { // nil でないことをチェック
if age >= 18 {
print("成人です。")
}
}
このとき、 if age != nil
の { }
の中には age
が nil
でないときしか入りません。 age
が nil
でないことがわかっているので、 age
を使っても安心です。 age >= 18
でクラッシュすることもありません。
オプショナル型がない場合に問題となるのは、 うっかり age != nil
のようなチェック( nil
チェックと呼びます)を書き忘れてしまったときに、プログラムがクラッシュしてしまう ことです。もしコンパイラが「 nil
チェックを忘れてるよ!」と教えてくれたらミスが防げてうれしいですね。それをやってくれるのがオプショナル型です。
オプショナル型があるとどうなる?
では、オプショナル型があるとコードはどう変わるでしょうか。まずは、うっかり nil
チェックを忘れてしまった場合を見てみましょう。
// オプショナル型が『ある』とき
let age: Int? = Int(text)
if age >= 18 { // ⛔ コンパイルエラー
print("成人です。")
}
まず、 age
の型が Int
から Int?
に変わりました。 Int(text)
は失敗して nil
を返すかもしれないので、 age
の値は Int
型または nil
です。 Int
型の変数・定数に nil
を代入することはできないので、 age
は Int
型ではなく Int?
型でなければなりません。
オプショナル型を導入して Int
型と Int?
型を区別することで、コンパイラがうっかりミスに気付くことができます。 age >= 18
というコードは、 Int?
型の age
と Int
型の 18
を比較しています。 age
は nil
かもしれないので、 age >= 18
という比較はうまくいかないかもしれません。コンパイラはこのコードはおかしいと判断して、コンパイルエラーにします。
プログラムを実行するためには必ずコードをコンパイルしないといけないので、コードを書いた人はプログラムを実行する前にミスに気付くことができます。コンパイルエラーを元に、自分の書いたコードの何がおかしかったんだろうと考えられます。そして、「 Int?
型の age
と Int
型の 18
を比較したのがよくなかったんだ!」と気付くことができるわけです。
ミスに気付いたら、今度はコードを↓のように変更するでしょう。 nil
チェックをして age
が nil
でないことを確かめます。しかし、それでもコンパイルエラーになってしまいます。
// オプショナル型が『ある』とき
let age: Int? = Int(text)
if age != nil {
if age >= 18 { // ⛔ コンパイルエラー
print("成人です。")
}
}
今度はきちんと age != nil
で nil
チェックをしています。 age >= 18
の時点では age
が nil
でないことは確実です。どうしてコンパイルエラーになってしまうのでしょうか。
コンパイラがチェックしているのは age >= 18
という式の型だけです。 age != nil
をチェックしたからといって、 age
の型が Int?
から Int
に変わるわけではありません。 if age != nil
の { }
の中でも、 age
の型は Int?
のままです。そのため、 age >= 18
は相変わらず Int?
と Int
を比較していることになり、コンパイルエラーになってしまうのです。
では、どうすればよいのでしょうか。ここで強制アンラップ( Forced Unwrapping )の出番です。 !
を使って強制アンラップすれば、コンパイルして実行することができるようになります。 ( 後ほど説明しますが、これは良い方法ではありません。)
// オプショナル型が『ある』とき
let age: Int? = Int(text)
if age != nil {
if age! >= 18 {
print("成人です。")
}
}
!
(強制アンラップ)は、オプショナル型を無理やりオプショナルでない型に変換します。 Int?
の age
に対し、 age!
と書けば Int
型に変換することができます。しかし、もし age
が nil
だった場合には実行時エラーになってプログラムがクラッシュします。
↑のコードでは、事前に age != nil
をチェックしているので age!
でクラッシュすることはありません。 "ABC"
のような文字列が入力されてもクラッシュせずに動くプログラムができました。めでたし、めでたし。
といきたいところですが、↑のコードにも問題があります。このコードを書いた人とは別の人が読む場合、(もしくは、すっかりコードの内容を忘れてしまった半年後の自分が読む場合、) age!
を見てドキッとします。 この age
は本当に nil
にならないのか、うっかりチェックを忘れてしまっていて、実はこのコードはクラッシュする可能性があるんじゃないか、と不安に思うことでしょう。そして、万が一チェックを忘れているとプログラムはクラッシュする可能性があります。 幸い、このコードでは前の行を見ると if age != nil
とチェックしていることが確認できるので、 age
は nil
にならないと安心することができます。
しかし、 nil
チェックがいつも強制アンラップ( !
)の近くに書かれているとは限りません。そもそも、 nil
チェックをしているので、この age! >= 18
の行では age
は絶対に nil
でないわけです。コンパイラが age
を Int?
型ではなく Int
型として扱ってくれればいいのに!そうすれば最初から !
を心配をする必要はないはずです。
つまり、僕らが必要としているのはただの nil
チェックではなく、 nil
チェックと同時に Int?
型から Int
型に変換してくれるような処理なのです。それをやってくれるのが if let
です。
if let
は何のためにあるのか
if let
を使えば、 nil
チェックと Int?
→ Int
の変換を同時に行うことができます。
// if let を使う場合
let age: Int? = Int(text)
if let age2: Int = age {
if age2 >= 18 {
print("成人です。")
}
}
↑のコードでは、新しい Int
型の定数 age2
を作って、そこに age
の値を代入しようとしています。 age
は Int?
型なので nil
の可能性があります。 nil
を( Int
型の) age2
に代入することはできません。
そのため、↑のコードでは age
が nil
でなかった場合だけ age2
への代入が行われます。もし age
が nil
だったら、 { }
の中身は実行されません。そのような条件分岐をするからこそ let
に if
が付いて if let
なのです。
if let
で宣言された age2
は、 { }
の中でだけ使うことができます1。 age2
は Int
型なので、 age2 >= 18
のように比較をしても Int
と Int
の比較なので問題ありません。
if let
がやっていることを整理すると、次の二つになります。
-
age
がnil
でないことのチェック(nil
チェック) -
Int?
型のage
をInt
型のage2
に変換
if let
を使えば、強制アンラップ( !
)のようにプログラムがクラッシュする心配もなく、またうっかり nil
チェックを忘れてしまうこともなくコードを書けるのです。オプショナル型のおかげで安全なコードが書けるようになりました。
なお、 if let
の正式な名前は Optional Binding と言います2。 if let
で通じますが、一応正式名称も覚えておきましょう。また、滅多に使うことはありませんが、 if let
だけでなく if var
を書くこともできます。もし定数ではなく変数であってほしい場合には if var
が使えます。
よりスマートな書き方
ところで、↑のコードはもっとスマートに書くことができます。まず、 if let age2: Int = age
の age2
が Int
型なのはわかりきっています。 : Int
がなくてもコードの読みやすさは変わらないでしょう。 if let age2 = age
で十分です(もちろん、型を書くことでコードが読みやすくなる場合は型を書いた方がです)。
また、 age2
を使わずに、 if let age = age
と書くことも可能です。 Swift ではスコープ( { }
の階層)が違えば同じ名前の変数・定数を作ることができます。たとえば、↓のようなコードを書くこともできます。
let a = 2
do {
let a = 3
print(a) // 3
}
print(a) // 2
このように、内側のスコープ( { }
)で外側のスコープと同じ名前の変数・定数を宣言して、外側の変数・定数を隠してしまうことを シャドーイング と呼びます。
if let
は一見 { }
の外側で宣言されているように見えますが、 { }
の中に所属します。そのため、 age
を シャドーイング して↓のように書けるわけです。
// if let を使う場合のスマートな書き方
let age: Int? = Int(text)
if let age = age { // シャドーイング
if age >= 18 {
print("成人です。")
}
}
さらに、スマートに書くこともできます。↑のコードでは if let age = age
と if age >= 18
が二つ続いてネストしています。この二つの条件を ,
で区切って並べて、一つの if
文にまとめることができます。
// if let を使う場合のスマートな書き方
let age: Int? = Int(text)
if let age = age, age >= 18 {
print("成人です。")
}
このような、 if let
してから値をチェックするは頻出パターンです。習得しておくと役に立つ機会も多いでしょう。
!
を使うべきとき
強制アンラップ( !
)は危険なもので、使わない方が良いのでしょうか。そうではありません。 !
を使うべきケースもあります。
たとえば、テキストフィールドに数字しか入力できない制限がかかっていたらどうでしょうか。 "ABC"
のような文字列を入力することはできません。もし、常に有効な整数しか入力できないのであれば、 Int(text)
が失敗して nil
を返すことはありません。
しかし、それがわかっていても、 Int(text)
が返すのは型の上では Int?
です。これはどうしようもありません。型は万能ではないので、絶対に起こらないけど型では表現できないこともあります。そんな場合には !
を使うことでコードを簡潔にできます3。
// text に有効な整数しか入力できない場合
let age: Int = Int(text)! // 強制アンラップ
if age >= 18 {
print("成人です。")
}
ただし、「絶対に有効な整数しか入力できない制限」の実装にバグがあると、↑のコードはクラッシュを引き起こす可能性があります。 !
は絶対に nil
にならないと確信できる場合のみ使うようにしましょう。
コード全体
テキストフィールドの作成や text
の宣言なども含めたコンパイル可能なコード全体
import SwiftUI
struct ContentView: View {
@State var text: String = ""
var body: some View {
VStack {
TextField("年齢", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
.frame(maxWidth: 200)
.padding(20)
Button.init("決定") {
let text: String = self.text
let age: Int? = Int(text)
if let age = age, age >= 18 {
print("成人です。")
}
}
}
}
}
↑のコードは SwiftUI を使って書かれています。ただし、 SwiftUI を正しく理解するためには Swift についての深い知識( Property Wrappers, Key Path Member Lookup, Function builders など)が必要とされます。個人的には初心者の間は SwiftUI よりも UIKit を使うことをオススメします。
まとめ
- オプショナル型がないとうっかり
nil
チェックを忘れてしまうかもしれない。nil
チェックを忘れるとプログラムはクラッシュしてしまう可能性がある。 - 強制アンラップ(
!
)を使ったコードは、そこだけを見て安全なのか判断できない。正しくnil
チェックが行われていないと強制アンラップは失敗しクラッシュする。 -
if let
はnil
チェックとアンラップを同時に行う。nil
チェック忘れを防止しながら、安全にオプショナル型から値を取り出すことができる。 -
!
は絶対に使ってはいけないわけではない。型は万能でないので、型の上ではオプショナルだけど、絶対にnil
にならないとわかっている場合には!
を使っても良い。
ステップアップ
-
SwiftのOptionalのベストプラクティス
- Swift のオプショナル型の使い方についてより深く学びたい場合
-
SwiftのOptional型を極める
- オプショナル型の仕組みや概念についての理解をより深めたい場合
-
厳密には、同じ
if let
の条件式の中でも使うことができます。(例:if let age = age, age >= 18 { ... }
) ↩ -
厳密には、 Optional Binding は
if let
だけでなく、if var
やguard let
、guard var
も含みます。 ↩ -
これは、スマホアプリのようにクライアントサイドで完結するケースを想定しています。サーバーサイドのプログラムの場合、クライアントサイドで入力できる値をどれだけ制限しても、リクエストで不正な値が送られる可能性を排除できません。サーバーサイドでは常に不正な値を想定して処理を記述する必要があります。 ↩