こんにちは。きんくまです。
今回はRxSwiftのmapとflatMapの違いについていまいちわかってなかったので、まとめです。
Rxは観測するObserverと、観測されるObservableという大事な概念があります。
で、それと同じくらい大事なのが、流れを作るSequenceと、その中を流れるElementということだと思いました。
これをイメージしておくと、とたんにわかりやすくなりました。
Observableというのは、SequenceでElementを流すやつ。みたいな。
PublishSubject/Single/Observable などは Sequence です。どんなイベントを発行するか決定します。逆にいうと、どのイベントを発行しないかというのもポイントになってきます。
そのイベントにともなって、Elementを流したりします。(completedのときなどelementを流さないイベントもあるのでこんな表現)
通常
こんな感じのasyncのSingleがあったとします。
0.5秒後にStringを返します。
func loadText() -> Single<String> { let single = Single<String>.create { event -> Disposable in DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.5) { event(.success("Hello")) } return Disposables.create() } return single }
普通に使います。
func normalExample() { loadText().subscribe { event in switch event { case .success(let text): print("success: \(text)") //Hello case .error(let error): print("error \(error)") } }.disposed(by: disposeBag) }
map
mapを使ってみます。
func mapExample() { loadText().map { $0 + " World" } .subscribe { event in switch event { case .success(let text): // text は String型 print("success: \(text)") // Hello World case .error(let error): print("error \(error)") } }.disposed(by: disposeBag) }
これは、loadTextで返ってくる値を加工して、subscribeの中に受け入れています。
で、私はJavaScriptのmapが頭にあったので、配列が流れてこないといけないと思っていたので、最初はよくわからなかったです。が、RxのSwiftは流れてくるElementが特に配列じゃなくても大丈夫でした。
なので、Rxのmapは、Elementを加工するためのものと考えてよいと思います。それは型の変換かもしれないし、さきほどのようにデータを直接いじるものかもしれません。実案件などでは、文字列からCodableプロトコルに適応したModelクラスを作るなんてのもできますね。
上と全く同じことをsubscribeの中でやります。
func mapExample2() { loadText().subscribe { event in switch event { case .success(let text): let outputText = text + " World" print("success: \(outputText)") // Hello World case .error(let error): print("error \(error)") } }.disposed(by: disposeBag) }
この方法でも全く問題ないです。ただ、subscribeの中に目的のデータの形に加工済みのものが入ってきた方が実装としてはきれいな気がします。
実際にやってみます。
例えばこんなstructがあるとします。
struct Message { let baseText: String var greetText: String { return baseText + " World" } init(baseText: String) { self.baseText = baseText } }
subscribeにこの型になったデータが入ってくるようにします。
func mapExampleConvertStruct() { loadText().map { Message(baseText: $0) } .subscribe { event in switch event { case .success(let message): // message は Message型 print("success: \(message.greetText)") //Hello World case .error(let error): print("error \(error)") } }.disposed(by: disposeBag) }
こうすることで、加工済みのデータが subscribe 内で処理できるようになりました!
flatMap
flatMapを使ってみます。
func flatMapExample() { loadText().flatMap { text -> Single<String> in let outputText = text + " World" return Single<String>.just(outputText) }.subscribe { event in switch event { case .success(let text): print("success: \(text)") // Hello World case .error(let error): print("error \(error)") } }.disposed(by: disposeBag) }
flatMapの中を注目して欲しいのですが、クロージャーの中で返す型が Single<String> になっています。
flatMapのクロージャーは、Sequenceの中からElementを一度取り出して、流れを消してしまいます。
なので、もう一度ElementをSequenceにくるんで流れを作って、返す必要があるみたいです。
まとめ
まとめると
map() は Sequence を操作しないで、Element だけを加工します。
flatMap() は Sequence を消して Element を取り出すので、また Sequenceを作成して返す必要があります。
おまけ:flatMapの実験
flatMap内のクロージャでSequenceを操作できることについて実験してみます。(例がよいのかわかりませんが、、、)
0.5秒後に一連のイベントとともにElementを発行するメソッドがあるとします。
func loadSplittedTexts() -> PublishSubject<String> { let subject = PublishSubject<String>() DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.5) { subject.onNext("hello") subject.onNext("world") subject.onNext("!") subject.onCompleted() } return subject }
受け取り側
loadSplittedTexts() .subscribe { event in switch event { case .next(let text): print("success: \(text)") case .error(let error): print("error \(error)") case .completed: print("completed") } }.disposed(by: disposeBag)
出力
success: hello success: world success: ! completed
flatMapでSequenceを操作してみる
flatMapで Sequence をいじってみます。
loadSplittedTexts() .flatMap { text -> Observable<String> in if text == "world" { return Observable<String>.empty() } return Observable<String>.just(text) } .subscribe { event in switch event { case .next(let text): print("success: \(text)") case .error(let error): print("error \(error)") case .completed: print("completed") } }.disposed(by: disposeBag)
出力
success: hello success: ! completed
“world” という出力がなくなっていました。
これはflatMap内で Sequenceを操作して、値の流れを止めてしまった(empty()を返した)ことによるからだと思います。
こんな感じにmapとflatMapの使い分けができるみたいです。ではでは。
■ 自作iPhoneアプリ 好評発売中!
・フォルメモ - シンプルなフォルダつきメモ帳
・ジッピー電卓 - 消費税や割引もサクサク計算!
■ LINEスタンプ作りました!
毎日使える。とぼけたウサギ