71
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Swift初心者のための】オプショナル型と if let は何のためにあるの?

Posted at

Swift の初心者向けの解説を読んでいると、次のような説明を見かけることがあります。

Int? のように ? を付けるとオプショナル型になります。オプショナル型の値を取り出すには ! を付けて強制アンラップします。

まちがってはいないのですが、 この説明では誤解を生みかねません 。オプショナル型が何のためにあるのかに立ち戻って、適切な使い方を説明します。

オプショナル型がないと何が起こる?

オプショナル型は何のためにあるのでしょうか。もし Swift にオプショナル型がなかったらどのような問題が起こるでしょうか。

例として、ユーザーに年齢を入力してもらい、成人か確認するコードを考えてみましょう。 18 歳以上の場合には 成人です。 と表示します。

text-field.png

年齢( age )が 18 歳以上かチェックするには if age >= 18 のような条件を書きます。しかし、テキストフィールドに入力された年齢は文字列なので String 型です。 age >= 18 のように比較するには、 ageString 型ではなく Int 型である必要があります。

StringInt に変換するには↓のように書きます( textString 型とします)。

let age = Int(text)

Int(text)textInt 型に変換し、その結果が age に代入されます。たとえば、テキストフィールドに "20" という文字列が入力されていたら age の値は整数の 20 になります。

text-field-20.png

しかし、もし "ABC" という文字列が入力された場合はどうなるでしょうか。 "ABC"Int (整数)に変換することはできません。そんな場合は、変換に失敗したことを表す nil になります。

text-field-abc.png

Swift では Int 型の変数や定数に nil を代入することができません。 nil を代入したい場合は ? を付けて Int? 型とします。これがオプショナル型です。 Int? 型なら Int 型の値か nil を、 String? 型なら String 型の値か nil を代入することができます。↑のコードでは ageInt? 型になります。

しかし、オプショナル型がない言語では、どんな型の変数にも nil を代入することができます。 Int 型でも String 型でも、常に nil が代入されているかもしれません。もしオプショナル型がなければ、↑のコードの ageInt 型(ただし nil かもしれない)ということになるでしょう。

このとき、成人かチェックして 成人です。 と表示するコードはどのように書けば良いでしょう。たとえば、↓のように書いたとします。

// オプショナル型が『ない』とき
let age: Int = Int(text)
if age >= 18 {
    print("成人です。")
}

オプショナル型がないなら、↑のコードはコンパイルに成功し、実行することができるでしょう。 ageInt 型なので、 age >= 18 のような比較は型の上では問題ありません。

しかし、テキストフィールドに "ABC" のような文字列が入力されたらどうなるでしょうか。 agenil なので、 nil >= 18 という比較をすることになります。 nil18 の大小関係を比較することはできないので、実行時エラーになってプログラムはクラッシュしてしまいます(実際に、オプショナル型を持たない言語ではそのような挙動になります)。

"ABC" のような文字列を入力された場合でもプログラムがクラッシュしないようにするには、 age >= 18 をする前に agenil でないことをチェックしておきます。

// オプショナル型が『ない』とき
let age: Int = Int(text)
if age != nil { // nil でないことをチェック
    if age >= 18 {
        print("成人です。")
    }
}

このとき、 if age != nil{ } の中には agenil でないときしか入りません。 agenil でないことがわかっているので、 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 を代入することはできないので、 ageInt 型ではなく Int? 型でなければなりません。

オプショナル型を導入して Int 型と Int? 型を区別することで、コンパイラがうっかりミスに気付くことができます。 age >= 18 というコードは、 Int? 型の ageInt 型の 18 を比較しています。 agenil かもしれないので、 age >= 18 という比較はうまくいかないかもしれません。コンパイラはこのコードはおかしいと判断して、コンパイルエラーにします。

プログラムを実行するためには必ずコードをコンパイルしないといけないので、コードを書いた人はプログラムを実行する前にミスに気付くことができます。コンパイルエラーを元に、自分の書いたコードの何がおかしかったんだろうと考えられます。そして、「 Int? 型の ageInt 型の 18 を比較したのがよくなかったんだ!」と気付くことができるわけです。

ミスに気付いたら、今度はコードを↓のように変更するでしょう。 nil チェックをして agenil でないことを確かめます。しかし、それでもコンパイルエラーになってしまいます。

// オプショナル型が『ある』とき
let age: Int? = Int(text)
if age != nil {
    if age >= 18 { // ⛔ コンパイルエラー
        print("成人です。")
    }
}

今度はきちんと age != nilnil チェックをしています。 age >= 18 の時点では agenil でないことは確実です。どうしてコンパイルエラーになってしまうのでしょうか。

コンパイラがチェックしているのは age >= 18 という式の型だけです。 age != nil をチェックしたからといって、 age の型が Int? から Int に変わるわけではありません。 if age != nil{ } の中でも、 age の型は Int? のままです。そのため、 age >= 18 は相変わらず Int?Int を比較していることになり、コンパイルエラーになってしまうのです。

では、どうすればよいのでしょうか。ここで強制アンラップ( Forced Unwrapping )の出番です。 ! を使って強制アンラップすれば、コンパイルして実行することができるようになります。 :warning: 後ほど説明しますが、これは良い方法ではありません。)

// オプショナル型が『ある』とき
let age: Int? = Int(text)
if age != nil {
    if age! >= 18 {
        print("成人です。")
    }
}

! (強制アンラップ)は、オプショナル型を無理やりオプショナルでない型に変換します。 Int?age に対し、 age! と書けば Int 型に変換することができます。しかし、もし agenil だった場合には実行時エラーになってプログラムがクラッシュします。

↑のコードでは、事前に age != nil をチェックしているので age! でクラッシュすることはありません。 "ABC" のような文字列が入力されてもクラッシュせずに動くプログラムができました。めでたし、めでたし。

といきたいところですが、↑のコードにも問題があります。このコードを書いた人とは別の人が読む場合、(もしくは、すっかりコードの内容を忘れてしまった半年後の自分が読む場合、) age! を見てドキッとします。 この age は本当に nil にならないのか、うっかりチェックを忘れてしまっていて、実はこのコードはクラッシュする可能性があるんじゃないか、と不安に思うことでしょう。そして、万が一チェックを忘れているとプログラムはクラッシュする可能性があります。 幸い、このコードでは前の行を見ると if age != nil とチェックしていることが確認できるので、 agenil にならないと安心することができます。

しかし、 nil チェックがいつも強制アンラップ( ! )の近くに書かれているとは限りません。そもそも、 nil チェックをしているので、この age! >= 18 の行では age は絶対に nil でないわけです。コンパイラが ageInt? 型ではなく 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 の値を代入しようとしています。 ageInt? 型なので nil の可能性があります。 nil を( Int 型の) age2 に代入することはできません。

そのため、↑のコードでは agenil でなかった場合だけ age2 への代入が行われます。もし agenil だったら、 { } の中身は実行されません。そのような条件分岐をするからこそ letif が付いて if let なのです。

if let で宣言された age2 は、 { } の中でだけ使うことができます1age2Int 型なので、 age2 >= 18 のように比較をしても IntInt の比較なので問題ありません。

if let がやっていることを整理すると、次の二つになります。

  • agenil でないことのチェック( nil チェック)
  • Int? 型の ageInt 型の age2 に変換

if let を使えば、強制アンラップ( ! )のようにプログラムがクラッシュする心配もなく、またうっかり nil チェックを忘れてしまうこともなくコードを書けるのです。オプショナル型のおかげで安全なコードが書けるようになりました。

なお、 if let の正式な名前は Optional Binding と言います2if let で通じますが、一応正式名称も覚えておきましょう。また、滅多に使うことはありませんが、 if let だけでなく if var を書くこともできます。もし定数ではなく変数であってほしい場合には if var が使えます。

よりスマートな書き方

ところで、↑のコードはもっとスマートに書くことができます。まず、 if let age2: Int = ageage2Int 型なのはわかりきっています。 : 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 = ageif 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 letnil チェックとアンラップを同時に行う。 nil チェック忘れを防止しながら、安全にオプショナル型から値を取り出すことができる。
  • ! は絶対に使ってはいけないわけではない。型は万能でないので、型の上ではオプショナルだけど、絶対に nil にならないとわかっている場合には ! を使っても良い。

ステップアップ

  1. 厳密には、同じ if let の条件式の中でも使うことができます。(例: if let age = age, age >= 18 { ... }

  2. 厳密には、 Optional Bindingif let だけでなく、 if varguard letguard var も含みます。

  3. これは、スマホアプリのようにクライアントサイドで完結するケースを想定しています。サーバーサイドのプログラムの場合、クライアントサイドで入力できる値をどれだけ制限しても、リクエストで不正な値が送られる可能性を排除できません。サーバーサイドでは常に不正な値を想定して処理を記述する必要があります。

71
36
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
71
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?