167
133

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 5 years have passed since last update.

SwiftのOptionalのベストプラクティス

Last updated at Posted at 2017-04-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 なのかは厳密に区別ができないこともあります。たとえば、配列が空のときに firstnil なのは、値が存在しないとも、最初の値を取り出そうとして失敗したとも解釈できます。

どのように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 を使うのが良いでしょう。 guardelse ブロックでは必ず脱出しないといけないので、 guard let だと if let と比べてネストしづらいですし、早期脱出のための分岐だと明確になり可読性も高まります。

後続の処理があり returnbreak しては困るケースも稀にありますが、大抵のケースでは 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! と書くと、 foonil だった場合にプログラムを停止させられます。そのようなエラーは 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はプロパティか関数に渡すときに使う

mapflatMap を使えば、 ?. では記述できないケースにも対応することができます。これは Swift 1 の頃には便利でした。

// `array.fisrt` の結果を引数として渡したい
array.first.map { Foo($0) }

しかし、 OptionalmapflatMap を使う機会は減りました。当時は言語サポートのある唯一のエラー処理手段が 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 で代替できないか一度考えてみましょう。

mapflatMap を使う機会があるとすると、戻り値である 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?.barOptional のまま値を取り回し、それをそのまま ?? に渡しています。

let bar = foo?.bar ?? Bar(42)

barfoo のメンバならシンプルに ?. で書けますが、 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)

ただし、 OptionalmapflatMap は可読性を損ねるおそれがあります。クロージャ式で複雑なことはしない、他の式と連結しないなど、最小限の使用に留めるのが望ましいと思います。

個人的には、↓のような構文があったらいいのになぁと考えています。

// `map` の代わりにこんな風に書けたらうれしい
let bar = Bar(foo: foo?) ?? Bar(42)

まとめ

いつOptionalを使うか

  • 値が存在しないかもしれないとき
  • Simple domain error が発生し得るとき

どのようにOptionalを使うか

  • Optional はできるだけ早く処理する
  • ! は unwrap 時に絶対に nil にならないときだけ使う
  • 複数の Optional をまとめて処理したくなったら要注意
  • map/flatMapは戻り値をプロパティか関数に渡すときに使う
167
133
17

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
167
133

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?