それでは、『SwiftのOptionalのベストプラクティス』というタイトルで発表します。 #swtws pic.twitter.com/OJfz2tk4sP
— koher (@koher) 2017年4月15日
2 年程前、 Qiita に『SwiftのOptional型を極める』というタイトルで投稿をしました。内容は、 Optional
とは何かという説明と、 Optional
の使い方を合わせたものでした。
しかし、 Swift 1 だった当時と比べると Optional
周りの状況は大きく変わり、特に使い方の部分が適切ではなくなってきました。また、僕のSwift経験値もたまって、新たな知見も蓄積されてきました。
今回の投稿では、 Swift の Optional
周りを整理し、いつ・どのように Optional
を使うべきか、僕の考えるベストプラクティスを紹介します。
全体を通して、以前投稿したSwiftのエラー4分類(Simple domain error、Recoverable error、Logic failure等)の考え方が出て来るので、適宜内容を参照して下さい。
いつOptionalを使うか
Optional
を使うのは、主に次の二つの場合です。
- 値が存在しないかもしれないとき
- Simple domain error が発生し得るとき
値が存在しないかもしれないとき
たとえば、会員登録時に年齢が任意項目なら、 User
インスタンスの age
の値は存在しないかもしれません。値が存在しないことを nil
で表すために Optional
を用います。
struct User {
var firstName: String
var lastName: String
var age: Int? // 値がない場合は `nil`
}
Simple domain errorが発生し得るとき
Simple domain error とは、ハンドリング時にエラーが発生したことさえわかれば良いというエラーです。前提条件が明確で、エラーの詳細な情報が不要な場合に適しています。
Simple domain error のわかりやすい例は、文字列を整数にパースする処理です。 "123"
という文字列は 123
に変換できますが、 "xyz"
のような文字列は整数として解釈できないのでパースに失敗します。前提条件が明確で、エラーの原因も明らかです。
そのような失敗を nil
で表すために Optional
を使います。エラー処理は Optional binding で行います。
guard let number = Int(string) else {
... // エラー処理
}
// `number` を使う処理
値が存在しないのか、 Simple domain error なのかは厳密に区別ができないこともあります。たとえば、配列が空のときに first
が nil
なのは、値が存在しないとも、最初の値を取り出そうとして失敗したとも解釈できます。
どのようにOptionalを使うか
Swift には Optional
関連の道具がたくさんあります。それらは便利ではありますが、どのような道具も使い方を間違えるとひどい目にあいます。そういうわけで、次に Optional
をどのように使うかについて説明します。
Optionalはできるだけ早く処理する
基本的に、 Optional
は値を得たらすぐに処理するのが望ましいです。 Optional
なプロパティはローカル変数に取り出してすぐに、 Simple domain error はエラー発生直後に処理をします。
⛔ Optionalのまま値を取り回す
let foo: Foo? = ...
let bar: Bar? = foo?.bar
guard let baz = bar.map { Baz($0) } else { ... }
// `baz` を使う処理
✅ Optionalをすぐに処理する
guard let foo = ... else { ... }
guard let bar = foo.bar else { ... }
guard let baz = Baz(bar) else { ... }
// `baz` を使う処理
nil
は値がないことしか表さないので、どういう経緯で値がnilになったのかを後から知ることができません。 Optional
のまま値を取り回して後で処理しようとすると、途中のどの段階で nil
が発生したのかわからず、デバッグする時にやっかいです。
みなさんも、 Optional
のまま値を取り回して想定外の nil
を得、その原因箇所を特定するのに苦労したことがあるんじゃないでしょうか。 Optional
値を得た直後に処理するようにしておけば、どこで nil
が発生したのか簡単にわかります。
Optional
の処理は基本的に guard let
を使うのが良いでしょう。 guard
の else
ブロックでは必ず脱出しないといけないので、 guard let
だと if let
と比べてネストしづらいですし、早期脱出のための分岐だと明確になり可読性も高まります。
後続の処理があり return
や break
しては困るケースも稀にありますが、大抵のケースでは guard
で書けるはずです。一見、早期脱出できないように見えても、その部分を関数に切り出した方が適切だということも多いです。
guard
が使えないと思ったら、一度その関数・メソッドの実装を疑ってみるといいと思います。処理の粒度が大きすぎるのかもしれません。処理をより細かいいくつかの関数に分解できないか考えてみましょう。
⛔ 一見guardで書けなさそうなのでifで書く
let foo: Foo
// ⛔ Optional binding がネストする
if let bar = ... {
if let baz = ... {
foo = Foo(baz: baz)
} else {
foo = Foo(bar: bar)
}
} else {
foo = Foo(42)
}
// `foo` を使う後続処理
✅ 関数に切り出しguardで早期脱出する
// ✅ Optional binding がネストしない
func makeFoo(...) -> Foo {
guard let bar = ... else {
return Foo(42)
}
guard let baz = ... else {
return Foo(bar: bar)
}
return Foo(baz: baz)
}
let foo: Foo = makeFoo(...)
// `foo` を使う後続処理
処理を関数に切り出すとテスタビリティも上がります。このように複雑な条件分岐を伴う処理は、たとえ数行であっても関数に切り出して単体テストを書いた方が、結果的に時間を削減できることが多いです。
そういうわけで、僕は if let
よりも guard let
を好んで使っているんですが、一つ問題があります。シャドーイングができないことです。
let foo: Int? = 42
// これはできる
if let foo = foo { ... }
// けど、これはできない
guard let foo = foo else { return }
if let foo = foo
のように、元の変数と同じ名前で Optional binding するケースは多いと思います。 guard let
の場合、これができません。 if let
と違ってスコープが切られないためです。
これについては日常的に不便を感じているので、もし良い代替案を知っている方がいれば教えてもらえるとうれしいです。
なお、 if let
を使うケースとして、 Optional
値が nil
でないときの早期脱出があります。
if let _ = foo {
return
}
// `foo` が `nil` のときの処理
!はunwrap時に絶対にnilにならないときだけ使う
Swift には foo!
, Foo!
, try!
, as!
などいくつかの!があります。すべてに共通しているのは、絶対にその処理が失敗しないときに使うべきものだということです。
ここでは、 Optional
と関係のある次の二つについて説明します。
- Forced unwrapping (
foo!
) - Implicitly unwrapped optional (
Foo!
)
Forced unwrapping (foo!)
foo!
と書くと、 foo
が nil
だった場合にプログラムを停止させられます。そのようなエラーは Logic failure に分類されます。
Logic failure は、実行時にハンドリングして解決するエラーではありません。 Logic failure が起こったということはプログラム自体に何らかの問題があることを意味し、プログラムの修正によって解決を図ります。
裏を返せば、 Logic failure でクラッシュするのはプログラムのバグなので、 nil
になる可能性がある値に対して !
を使ってはならないということです。 !
は絶対に nil
にならないことがわかっている値にのみ利用すべきです。
では、絶対に nil
にならないとはどのようなケースでしょうか。絶対に nil
にならないのであれば、最初から Optional
にしなければ良いのではないでしょうか。
残念ながら型は万能ではありません。型の上では Optional
になるけれども、ロジック上、絶対に nil
にならないケースが存在します。そういう場合に Optional binding
で分岐するのは無駄です。!で非 Optional
値に変換します。
僕がいつも挙げる例なので聞き飽きた人もいるかもしれませんが、たとえば、テキストフィールドに入力された文字列を整数に変換する処理を考えて下さい。 UI の制約で数値しか入力できないとすると、この処理は絶対に失敗しません。
⛔ 絶対にnilでないケースで条件分岐する
// `numberField` には数値しか入力できない
guard let number = Int(numberField.text) else {
fatalError("Never reaches here.") // 無駄な処理
}
// `number` を使う処理
✅ 絶対にnilにならないなら!で非Optional値に変換する
// `numberField` には数値しか入力できない
let number = Int(numberField.text)!
// `number` を使う処理
なお、 Logic failure はコードの問題だけを意味するわけではありません。たとえば、アプリにバンドルされ必ず存在するはずの設定ファイルが存在しないというのも Logic failure です。
必ず存在するはずの設定ファイルが存在しないなら、設定ファイルを作成するのがその Logic failur eに対する正しい対処になります。ファイルが存在しなくてもクラッシュしないようにコードを修正するのではありません。
使い捨てのスクリプト等で!を使いたいケースはどう考えれば良いでしょうか。 CUI で文字列を入力させ、整数に変換する場合を考えてみましょう。 GUI のように入力可能な文字を制御することもできません。しかし、使い捨てのコードのためにわざわざ失敗ケースを考えたくありません。
入力された文字列は数値に変換できるとは限らないので、パースの結果が「絶対に nil
にならない」とは言い切れません。でも、こんな場合にも!は使いたいはずです。
これは僕の解釈ですが、 Logic failure の考え方を拡張すればこのようなケースにも適用できます。
この使い捨てスクリプトには UI 上の制約はありませんが、数値しか入力してはいけません。一般的なアプリでは、バグらないように正しく振る舞う責務はユーザーにはありません。しかし、このスクリプトではその責務をユーザーに負わせているわけです。
この場合、 !
でクラッシュしたなら、責務を果たさず前提条件を破ったのはユーザーです。つまり、ユーザーの Logic が失敗したのであってコードのバグではありません。ユーザーがバグってたのです。
バグってるのはユーザーなら、この Logic failure の正しい対処は、ユーザーが行動を改めることです。ユーザーが正しく振る舞えば、パース結果は絶対に nil
にはなりません。
このように解釈することで、「絶対に nil
にならない」ときにだけ!を使うという単一の基準で考えることができます。「絶対に nil
にならない」ことを担保するのが、コードなのか、ファイルや DB を含むシステム全体なのか、ユーザーなのかが変わるだけです。
Implicitly unwrapped optional (Foo!)
Foo!
も基本的には foo!
と同じように考えれば OK です。つまり、 unwrap 時に絶対に nil
にならないときだけ使うということです。
これは、 Implicitly unwrapped optional (以下 IUO )の挙動を考えれば明白です。 Foo!
は基本的に、 Foo?
型の値に対して常に !
を付けてアクセスするのと同じ挙動をします。
// これ↓と
let foo: Foo! = ...
let bar = foo.bar
baz(foo)
// これ↓は同じ
let foo: Foo? = ...
let bar = foo!.bar
baz(foo!)
Forced unwrapping の失敗は Logic failure で、絶対に nil
にならないときにだけ使えるものでした。 IUO では自動的にそれが実行されるということは、 unwrap される箇所では絶対に nil
になっていてはいけないことを意味します。
IUO の典型的なユースケースは IBOutlet です。 Swift の言語仕様上、 Optional
でないプロパティはイニシャライザで初期化されなければなりません。しかし、 IBOutlet はその仕組みの都合上、イニシャライザで初期化を実行することができません。
現実的には、 viewDidLoad
が呼ばれるときには IBOutlet なプロパティは初期化されており、それらを利用するときに nil
になっていることはありません。つまり、ロジック上「絶対に nil
にならない」わけです。
もしそのプロパティが普通の Optional
だと、「絶対に nil
にならない」ことがわかっているので、すべての箇所で !
を付けて Forced unwrapping しなければなりません。これは無駄です。そんなときには型レベルで !
を付けられれば便利です。
もし IUO に対して nil
チェックをしたり、 Optional binding を書いたりしていたら、設計がまずいのではないかと疑ってみましょう。その値は IUO ではなく Optional
であるべきかもしれません。
次のようなケースはどうでしょうか。 viewDidAppear
から viewWillDisappear
の間では「絶対に nil
にならない」ことが保証されており 99% はそこからアクセスされる、けれども、一箇所だけ viewWillAppear
からアクセスする必要がある。
そのようなプロパティは IUO にして、 viewWillAppear
で IUO に対して nil
チェックしたり Optional binding すべきでしょうか。それとも、普通の Optional
にして 99% のアクセスに!を付けるべきでしょうか。
僕は、委譲が答えになるんじゃないかと考えています。問題は、 ViewController
のライフサイクルとそのプロパティのライフサイクルのずれです。そのプロパティを使う処理を別の型に委譲してしまい、その型では非 Optional
値として保持すれば良いのです。
⛔ IUOをOptional bindingして分岐する
class ViewController: UIViewController {
// 99% は `nil` でないので IUO にする
var foo: Foo!
...
override func viewWillAppear(_ animated: Bool) {
...
// ⛔ IUO を Optional binding
guard let foo = self.foo { return }
// `foo` を使う処理
}
override func viewDidAppear(_ animated: Bool) {
...
foo = Foo(...)
}
override func viewWillDisappear(_ animated: Bool) {
... // `foo` が開放されるかもしれない処理
}
func useFoo(...) {
// `foo` を使う処理
// ✅ Forced unwrappingでなくて良い
foo.bar(...)
foo.baz(...)
foo.qux(...)
...
}
}
⛔ たった一箇所のためにForced unwrappingだらけにする
class ViewController: UIViewController {
// IUO を Optional binding したくないので Optional にする
var foo: Foo?
...
override func viewWillAppear(_ animated: Bool) {
...
// ✅ 普通の Optional の binding
guard let foo = self.foo { return }
// `foo` を使う処理
}
override func viewDidAppear(_ animated: Bool) {
...
foo = Foo(...)
}
override func viewWillDisappear(_ animated: Bool) {
... // `foo` が開放されるかもしれない処理
}
func useFoo(...) {
// `foo` を使う処理
// ⛔ たった一箇所のために Forced unwrapping だらけ
foo!.bar(...)
foo!.baz(...)
foo!.qux(...)
...
}
}
✅ スコープを切って委譲する
class ViewController: UIViewController {
struct FooUser {
let foo: Foo
func useFoo(...) {
// `foo` を使う処理
// ✅ Forced unwrappingでなくて良い
foo.bar(...)
foo.baz(...)
foo.qux(...)
...
}
}
var fooUser: FooUser?
...
override func viewWillAppear(_ animated: Bool) {
...
// ✅ 普通の Optional の binding
guard let foo = self.fooUser?.foo { return }
// `foo` を使う処理
}
override func viewDidAppear(_ animated: Bool) {
...
fooUser = FooUser(foo: Foo(...))
}
override func viewWillDisappear(_ animated: Bool) {
... // `fooUser` が開放されるかもしれない処理
}
func useFoo(...) {
// ✅ ここでは絶対に `nil` でないので Forced unwrapping
fooUser!.useFoo(...)
}
}
複数のOptionalをまとめて処理したくなったら要注意
Swift には便利な構文が用意されているので、複数の Optional
をまとめて処理できます。しかし、まとめて処理するということは nil
の発生源をわかりづらくしてしまうということでもあります。
複数の Optional
は、独立して並列に現れるケースと、直列した一連の処理の中で現れることがあります。
🤔 A: 並列な複数のOptionalをまとめて処理する
// 🤔 `a` と `b` のどちらが `nil` かわからない
guard let a = ...,
let b = ... else { ... }
let sum = a + b
🤔 B: 直列な複数のOptionalをまとめて処理する (1)
// 🤔 `array` と `first` のどちらが `nil` かわからない
guard let foo = self.array?.first?.foo else { ... }
// `foo` を使うコード
🤔 C: 直列な複数のOptionalをまとめて処理する (2)
// 🤔 `text` と `Int` のどちらが `nil` かわからない
guard let text = input.text,
let number = Int(text) else { ... }
// `number` を使うコード
これらは一概に悪いとは言えません。 nil
の発生源はわかりづらくなるのでデバッガビリティは低下しますが、コードはすっきりして書きやすいです。個別に処理をすることもできますが、やや冗長に感じられます。
🤔 A': 並列な複数のOptionalを個別に処理する
// 🤔 `guard` を 2 回書かないといけなくてすっきりしない
guard let a = ... else { ... }
guard let b = ... else { ... }
let sum = a + b
🤔 B': 直列な複数のOptionalを個別に処理する (1)
// 🤔 元々一つの式だったのが二つに分かれてしまった
guard let array = self.array else { ... }
guard let foo = array.first?.foo else { ... }
// `foo` を使うコード
🤔 C': 直列な複数のOptionalを個別に処理する (2)
// 🤔 `guard` を 2 回書かないといけなくてすっきりしない
guard let text = input.text else { ... }
guard let number = Int(text) else { ... }
// `number` を使うコード
可読性はどちらも大きく変わりませんし、どちらを採用すべきかはケース・バイ・ケースです。ただし、まとめて処理される Optional
の数が多くなるほどデバッガビリティを損ねるので注意しましょう。
複数の Optional
をまとめて処理するのは必ずしも悪いことではありません。しかし、複数の Optional
をまとめて処理したくなったら、本当にそれが良いのか、一度立ち止まって考えてみましょう。
map/flatMapはプロパティか関数に渡すときに使う
map
や flatMap
を使えば、 ?.
では記述できないケースにも対応することができます。これは Swift 1 の頃には便利でした。
// `array.fisrt` の結果を引数として渡したい
array.first.map { Foo($0) }
しかし、 Optional
の map
や flatMap
を使う機会は減りました。当時は言語サポートのある唯一のエラー処理手段が Optional
でした。そのため、 Simple domain error も Recoverable error も nil
で表していました。
Recoverable error は、発生の前提条件が明確でなく様々な原因で発生し、回復処理を記述させたいようなエラーです。ファイル入出力やネットワークのエラーが典型的な例です。
Simple domain error と違って、 Recoverable error は発生後すぐ個別に処理をするのではなく、合成してまとめて処理したり、伝播させたりしたいことが多くあります。
nil
発生源がわかりづらいという弊害があっても、 Recoverable error のために、当時は Optional
のまま値を取り回したい局面がありました。しかし、今は違います。 Recoverable error には throws
を使い、 Optional
は使いません。
map
/flatMap
は、 Optional
のまま値を取り回すには便利ですが、 Optional
をすぐ処理するのであれば可読性を損ねるだけです。
⛔ Optional binding前にmapで変換する
// ⛔ 一見して何をしているのかわかりづらい
guard let foo = dictionary["a"].map { Foo($0) } else { ... }
// `foo` を使う処理
✅ Optional binding後にunwrapされた値を変換する
// ✅ 処理の内容が明確で可読性が高い
guard let a = dictionary["a"] else { ... }
let foo = Foo(a)
// `foo` を使う処理
⛔ 一連の処理の中で発生する複数のOptionalを合成してからOptional bindingする
// ⛔ 一見して何をしているのかわかりづらい
guard let number = self.string.flatMap { Int($0) } else { ... }
// `number` を使うコード
✅ 一連の処理の中で発生する複数のOptionalを個別にOptional bindingする
// ✅ 処理の内容が明確で可読性が高い
guard let string = self.string,
let number = Int(string) else { ... }
// `number` を使うコード
複数の Optional
を一度に binding するのは、 Swift の構文の中で Haskell の do 記法に最も近いものです。 Haskell ですらモナドを快適に扱うのに do 記法を必要としています。 flatMap
だけでモナディックなコードを書こうとするのは無謀です。
Optional
に対して flatMap
を書きたくなったら、 Optional binding で代替できないか一度考えてみましょう。
map
や flatMap
を使う機会があるとすると、戻り値である Optional
値をそのまま利用したいときです。主に次の二つのケースが考えられます。
-
Optional
なプロパティに代入する -
Optional
な引数を持つ関数・メソッドに渡す
どちらの場合も注意が必要です。一歩間違えれば Optional
をすぐに処理せず、そのまま値を取り回すことにつながりかねません。
何らかの処理の失敗を表すために、 Optional
なプロパティで nil
を保持するのは得策ではありません。それは、エラーを Optional
のまま取り回していることになります。 Optional
なプロパティは、値がないことを表すために使いましょう。
map
/flatMap
の結果をそのままプロパティに代入したいケースとして、 setter 的なメソッドの引数で受けた Optional
値をそのままプロパティに保持するのではなく、変換してからプロパティで保持したいケースが考えられます。
⛔ Optional値の変換のためにわざわざ分岐する
init(foo: Foo? = nil) {
// ⛔ コードが複雑になる
if let foo = foo {
self.bar = Bar(foo)
} else {
self.bar = nil
}
}
✅ mapを使ってOptionalのまま値を変換する
init(foo: Foo? = nil) {
// ✅ シンプルに書ける
self.bar = foo.map { Bar($0) }
}
関数やメソッドの引数に Optional
値をとる場合も、 Optional
のまま値を取り回すことになっていないか要注意です。引数に渡す前に Optional
を処理する実装の方が良いかもしれません。 Optional
を引数にとる設計が適切か再度考えてみましょう。
引数で Optional
受けたいよくあるケースは、省略可能な引数を作る場合だと思います。しかし、このような引数に map
/flatMap
の結果をそのまま渡すことは稀だと思います。
class func animate(withDuration duration: TimeInterval,
animations: @escaping () -> Void,
completion: ((Bool) -> Void)? = nil)
Optional
をそのまま引数に渡す代表的な例が ??
です( Swift の演算子は関数です)。↓の例では foo?.bar
で Optional
のまま値を取り回し、それをそのまま ??
に渡しています。
let bar = foo?.bar ?? Bar(42)
bar
が foo
のメンバならシンプルに ?.
で書けますが、 unwrap した値を関数やメソッドの引数に渡したい場合はそうはいきません。そのような場合には ?.
の代わりに map
を使えます。分岐すると複雑なコードも、 map
/flatMap
ですっきり書けます。
⛔ デフォルト値のために条件分岐書く
// ⛔ コードが複雑になる
let bar: Bar
if let foo = foo {
bar = Bar(foo)
} else {
bar = Bar(42)
}
✅ mapの結果を??に渡す
// ✅ シンプルに書ける
let bar = foo.map { Bar(foo: $0) } ?? Bar(42)
ただし、 Optional
の map
や flatMap
は可読性を損ねるおそれがあります。クロージャ式で複雑なことはしない、他の式と連結しないなど、最小限の使用に留めるのが望ましいと思います。
個人的には、↓のような構文があったらいいのになぁと考えています。
// `map` の代わりにこんな風に書けたらうれしい
let bar = Bar(foo: foo?) ?? Bar(42)
まとめ
いつOptionalを使うか
- 値が存在しないかもしれないとき
- Simple domain error が発生し得るとき
どのようにOptionalを使うか
-
Optional
はできるだけ早く処理する -
!
は unwrap 時に絶対にnil
にならないときだけ使う - 複数の
Optional
をまとめて処理したくなったら要注意 -
map
/flatMap
は戻り値をプロパティか関数に渡すときに使う