最初は、Optional
のmap
とflatMap
の話だけ書いていたのですが、だんだんOptional
そのものの話まで膨らんできたので、一緒に書くことにしました。
今回の話はコードレビューで見つけたら割と指摘したい部分ではあるものの、毎回この内容を書くのも無理があるので、一度まとめておく価値はあるかなと思いました。
Optional
自体は、今では様々な言語に取り入れられているので、Swiftに限定しなくても良かったのですが、もろもろのシンタックスシュガーと歴史のためか、割と使う頻度が高い上に乱用も多いと思ったので、今回はSwift限定にしました。
Optionalの復習
他の言語でも多い実装ですが、Optional
の実装はただのenum
です。
簡単に書き直すとこのような感じになります。
enum Optional<Wrapped> { case none case some(Wrapped) init(_ some: Wrapped) { self = .some(some) } }
普段使っているT?
はOptional<T>
の、nil
は.none
のシンタックスシュガーです。
これらのシンタックスシュガーに隠れているため、Optional
がenum
であることを意識することは少ないかもしれないですが、たとえば、
enum Hoge { case none case ok }
という定義をすると、Hoge.none
を使おうとして、
var hoge: Hoge? = .none
と書くとOptional.none
が入ってしまうというミスが生じます。
このため、case none
を定義するのは危険で、Xcode 11から上のようなあいまいな.none
の使い方をすると警告が出るようになりました。(SR-2176)
おまけ: Xcode 11からOptionalにswitchが使えるようになった
知ってると良いことあるかもしれません。(SR-7799)
let hoge: Hoge? // Before if let hoge = hoge { switch hoge { case .ok: return 200 default: return 400 } } // After switch hoge { case .ok: return 200 default: return 400 }
可能な限りOptionalでない方が嬉しい
Optional
自体は強力な機能ですが、使いすぎてしまうことでデメリットもあります。
とくにOptional
を公開するインターフェースに使う場合、不都合が生じることがあります。
デメリットが生じる一つの例として、プロパティや戻り値を不必要にOptional
にすることで、それを使う人毎回unwrapするコストが発生してしまうケースがあります。
後で具体的なコードが出てきますが、Optional
でなくても成り立つものは、Optional
にしない方がインターフェースとしては優れています。
(もちろん例外を握りつぶすとか、force unwrapしろということではないです。)
引数のOptional
についても、map
やflatMap
を知っていれば減らせる場合があります。
些細な差かもしれないですが、パフォーマンス的にも値をラップする分Optional
の方がオーバーヘッドが生じ不利になります。
Optionalを使う時の方針
基本的な方針はOptional
を引き回さないということと、不用意にOptional
を生じさせないということです。
引き回しそうになった場合は、unwrapの処理を他のメソッドに任せず、if let
やguard let
、Optional.map
などを使ってなるべく外側で処理するようにします。
また、Optional
を発生させて戻り値に利用するメソッドや、failable initializer(init?
)を使う際には注意が必要です。
mapとflatMapの使い方
Optional
のmap
とflatMap
を活用することで、コードがシンプルになるケースや、Optional
が減るケースがあります。
mapの実装
map
は与えられたクロージャで中身を変換して、Optional
でラップします。
func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U? { switch self { case .some(let y): return .some(try transform(y)) case .none: return .none } }
中身(Wrapped
)をU
に変換するクロージャを渡すと、U?
が返ってきます。
flatMapの実装
それに対してflatMap
は、
func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U? { switch self { case .some(let y): return try transform(y) case .none: return .none } }
中身(Wrapped
)をOptional<U>
に変換するクロージャを渡すと、Optional<U>
が返ってきます。
mapとflatMapの使い分け
map
は(Wrapped) -> U
に対してU?
を返すので、もしU
がHoge?
だった場合、Hoge??
が返ります。
let urlString: String? = "hoge" let url = urlString.map(URL.init(string:)) // Hoge??
上のようなコードを書いた場合、url
の型はURL??
と2重のOptional
になってしまいます。
多くの場合2重にする必要性はないため、Optional
を返すクロージャに対しては、flatMap
を使うことが多いです。
let urlString: String? = "hoge" let url = urlString.flatMap(URL.init(string:)) // Hoge?
urlString.flatMap(URL.init(string:))
はurlString.flatMap { URL(string: $0) }
とほぼ同じです。
あまり良くないインターフェース
ありがちなケースを2つほど紹介します。
nilを伝播させるだけのインターフェースはアンチパターン
Optional
を引数にとって、nil
をそのまま返り値のOptional
に伝播させるだけのインターフェースはアンチパターンです。
func generateMessage(count: Int?) -> String? { guard let count = count else { return nil } return "あなたは\(count)人目の訪問者です" }
かなり極端な例ですが、上のようなメソッドは良いインターフェースとは言えません。
Optional
を渡すだけなら困りませんが、下のコードのように引数に、ただのInt
を渡した際にもOptional
が返ってきてしまいます。
let message = generateMessage(count: 10) // String?
1引数の場合は、guard
文のところはOptional.map
と全く同じことをやっているので、元のメソッドは単純に、
func generateMessage(count: Int) -> String { return "あなたは\(count)人目の訪問者です" }
として、Int?
を渡したいときは上のコードのようにすれば同じことができます。
こちらはInt
を渡したときもString
が返ってくるので、インターフェースとしては最初のものよりも優れています。
let count: Int? = 10 let message = count.map(generateMessage(count:))
また、複数引数がある場合も、同じように不必要にOptional
を発生させるケースがあるので、nil
を伝播させるためだけに引数や戻り値にOptional
を使うのは良いインターフェースとは言えません。
// Optionalだらけ func generateMessage(first: String?, second: String?) -> String? { guard let first = first, let second = second else { return nil } return "\(first) feat. \(second)" }
ただ、map
、flatMap
を使う場合は若干見づらくなるので、引数が多い場合はif let
やguard let
を使うほうが良いと思います。
func generateMessage(first: String, second: String) -> String { return "\(first) feat. \(second)" } let first: String? = "John" let second: String? = "Paul" if let first = first, let second = second { let message = generateMessage(first: first, second: second) // ... } // or let message = first.flatMap { str in second.map { generateMessage(first: str, second: $0) }}
このようにできるだけ外でunwrapするようにしておけば、引数がそれぞれnil
だった際のハンドリングも個別に分けることができるため、メソッドの中でunwrapするよりも柔軟に対応できます。
(逆に、上のような書き方は2つのnil
を1つにまとめてしまっているので、ある意味握りつぶしてしまっています)
failable intializer
failable initializerは、IUOやforce unwrapを使わない限り、必ずインスタンスをOptional
で持つことを強制します。
とくに、failable initializerしか用意されていないインターフェースにした場合、使う側にとってはインスタンスを作るごとに毎回unwrapしなくてはいけないため、非常にコストになります。
この点から、特定の状況以外では使わない方が良いと個人的には考えています。
具体的に例外のケースは、
super.init
がfailable- イニシャライザの中で複数
Optional
を使うメソッドやthrow
するメソッドがある場合
くらいかと思います。
1つ目のケースはrequired init?(coder:)
のように、スーパークラスがfailable initializerを持つクラスです。
2つ目のケースはJSONのデコードをイニシャライザの中で行うレスポンスのデータ型のような、複数Optional
が登場して、なおかつどれか失敗したら全体をnil
としたいクラスです。
特に、中でOptional
やtry?
を使っていないにも関わらず、init?
を使っていたら危険信号です。
final class Hoge { init?(members: [Member]) { guard let !members.isEmpty else { return nil } // 余計なお世話じゃ // ... } }
長いのでまとめ
上記のような、必要ない場面でOptional
を使ってしまうケースさえ気をつければ、非常に強力な機能なので、うまく付き合っていきましょう。