最初は、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を使ってしまうケースさえ気をつければ、非常に強力な機能なので、うまく付き合っていきましょう。