本文首發於 Ficow Shen's Blog,原文地址: Combine 框架,從0到1 —— 5.Combine 常用操作符。
內容概覽
- 前言
- breakpoint
- handleEvents
- map
- flatMap
- eraseToAnyPublisher
- merge
- combineLatest
- zip
- setFailureType
- switchToLatest
- 總結
前言
正所謂,工欲善其事,必先利其器。在開始使用 Combine
進行響應式程式設計之前,建議您先了解 Combine
為您提供的各種釋出者(Publishers)、操作符(Operators)、訂閱者(Subscribers)。
Combine 操作符(Operators) 其實是釋出者,這些操作符釋出者的值由上游釋出者提供。操作符封裝了很多常用的響應式程式設計演算法,有一些可以幫助我們更輕鬆地進行除錯,而另一些可以幫助我們更輕鬆地通過結合多個操作符來實現業務邏輯,本文將主要介紹這兩大類操作符。
後續示例程式碼中出現的 cancellables
均由 CommonOperatorsDemo
例項提供:
final class CommonOperatorsDemo {
private var cancellables = Set<AnyCancellable>()
}
官網文件:https://developer.apple.com/documentation/combine/publishers/print
print
操作符主要用於列印所有釋出的事件,您可以選擇為輸出的內容新增字首。
print
會在接收到以下事件時列印訊息:
- subscription,訂閱事件
- value,接收到值元素
- normal completion,正常的完成事件
- failure,失敗事件
- cancellation,取消訂閱事件
示例程式碼:
func printDemo() {
[1, 2].publisher
.print("_")
.sink { _ in }
.store(in: &cancellables)
}
輸出內容:
_: receive subscription: ([1, 2])
_: request unlimited
_: receive value: (1)
_: receive value: (2)
_: receive finished
breakpoint
官網文件:https://developer.apple.com/documentation/combine/publishers/breakpoint
breakpoint
操作符可以傳送除錯訊號來讓偵錯程式暫停程式的執行,只要在給定的閉包中返回 true
即可。
示例程式碼:
func breakpointDemo() {
[1, 2].publisher
.breakpoint(receiveSubscription: { subscription in
return false // 返回 true 以丟擲 SIGTRAP 中斷訊號,偵錯程式會被調起
}, receiveOutput: { value in
return false // 返回 true 以丟擲 SIGTRAP 中斷訊號,偵錯程式會被調起
}, receiveCompletion: { completion in
return false // 返回 true 以丟擲 SIGTRAP 中斷訊號,偵錯程式會被調起
})
.sink(receiveValue: { _ in
})
.store(in: &cancellables)
}
您可能會好奇,為什麼需要用這個操作符來實現斷點,為何不直接打斷點呢?
從上面的示例程式碼中,我們可以看出,通過使用 breakpoint
操作符,我們可以很容易地在訂閱操作、輸出、完成發生時啟用斷點。
如果這時候想直接在程式碼上打斷點,我們就要重寫 sink
部分的程式碼,而且無法輕易地為訂閱操作啟用斷點。
handleEvents
官網文件:https://developer.apple.com/documentation/combine/publishers/handleevents
handleEvents
操作符可以在釋出事件發生時執行指定的閉包。
示例程式碼:
func handleEventsDemo() {
[1, 2].publisher
.handleEvents(receiveSubscription: { subscription in
// 訂閱事件
}, receiveOutput: { value in
// 值事件
}, receiveCompletion: { completion in
// 完成事件
}, receiveCancel: {
// 取消事件
}, receiveRequest: { demand in
// 請求需求的事件
})
.sink(receiveValue: { _ in
})
.store(in: &cancellables)
}
handleEvents
接受的閉包都是可選型別的,所以我們可以只需要對感興趣的事件進行處理即可,不必為所有引數傳入一個閉包。
map
官網文件:https://developer.apple.com/documentation/combine/publishers/map
map
操作符會執行給定的閉包,將上游釋出的內容進行轉換,然後再傳送給下游訂閱者。和 Swift 標準庫中的 map
函式類似。
示例程式碼:
func mapDemo() {
[1, 2].publisher
.map { $0.description + $0.description }
.sink(receiveValue: { value in
print(value)
})
.store(in: &cancellables)
}
輸出內容:
11
22
flatMap
官網文件:https://developer.apple.com/documentation/combine/publishers/flatmap
flatMap
操作符會轉換上游釋出者傳送的所有的元素,然後返回一個新的或者已有的釋出者。
flatMap
會將所有返回的釋出者的輸出合併到一個輸出流中。我們可以通過 flatMap
操作符的 maxPublishers
引數指定返回的釋出者的最大數量。
flatMap
常在錯誤處理中用於返回備用釋出者和預設值,示例程式碼:
struct Model: Decodable {
let id: Int
}
func flatMapDemo() {
guard let data1 = #"{"id": 1}"#.data(using: .utf8),
let data2 = #"{"i": 2}"#.data(using: .utf8),
let data3 = #"{"id": 3}"#.data(using: .utf8)
else { fatalError() }
[data1, data2, data3].publisher
.flatMap { data -> AnyPublisher<CommonOperatorsDemo.Model?, Never> in
return Just(data)
.decode(type: Model?.self, decoder: JSONDecoder())
.catch {_ in
// 解析失敗時,返回預設值 nil
return Just(nil)
}.eraseToAnyPublisher()
}
.sink(receiveValue: { value in
print(value)
})
.store(in: &cancellables)
}
輸出內容:
Optional(CombineDemo.CommonOperatorsDemo.Model(id: 1))
nil
Optional(CombineDemo.CommonOperatorsDemo.Model(id: 3))
錯誤處理在響應式程式設計中是一個重點內容,也是一個常見的坑!一定要小心,一定要注意!!!
如果沒有 catch
操作符,上面的事件流就會因為 data2
解析失敗而終止。
比如,現在將 catch
去掉:
[data1, data2, data3].publisher
.setFailureType(to: Error.self)
.flatMap { data -> AnyPublisher<Model?, Error> in
return Just(data)
.decode(type: Model?.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
此時,輸出內容變為了:
Optional(CombineDemo.CommonOperatorsDemo.Model(id: 1))
failure(Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "id", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"id\", intValue: nil) (\"id\").", underlyingError: nil)))
最終,下游訂閱者因為上游發生了錯誤而終止了訂閱,下游便無法收到 Optional(CombineDemo.CommonOperatorsDemo.Model(id: 3))
。
eraseToAnyPublisher
官網文件:https://developer.apple.com/documentation/combine/anypublisher
eraseToAnyPublisher
操作符可以將一個釋出者轉換為一個型別擦除後的 AnyPublisher
釋出者。
這樣做可以避免過長的泛型型別資訊,比如:Publishers.Catch<Publishers.Decode<Just<JSONDecoder.Input>, CommonOperatorsDemo.Model?, JSONDecoder>, Just<CommonOperatorsDemo.Model?>>
。使用 eraseToAnyPublisher
操作符將型別擦除後,我們可以得到 AnyPublisher<Model?, Never>
型別。
除此之外,如果需要向呼叫方暴露內部的釋出者,使用 eraseToAnyPublisher
操作符也可以對外部隱藏內部的實現細節。
示例程式碼請參考上文 flatMap
部分的內容。
merge
官網文件:https://developer.apple.com/documentation/combine/publishers/merge
merge
操作符可以將上游釋出者傳送的元素合併到一個序列中。merge
操作符要求上游釋出者的輸出和失敗型別完全相同。
merge
操作符有多個版本,分別對應上游釋出者的個數:
- merge
- merge3
- merge4
- merge5
- merge6
- merge7
- merge8
示例程式碼:
func mergeDemo() {
let oddPublisher = PassthroughSubject<Int, Never>()
let evenPublisher = PassthroughSubject<Int, Never>()
oddPublisher
.merge(with: evenPublisher)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
oddPublisher.send(1)
evenPublisher.send(2)
oddPublisher.send(3)
evenPublisher.send(4)
}
輸出內容:
1
2
3
4
combineLatest
官網文件:https://developer.apple.com/documentation/combine/publishers/combinelatest
combineLatest
操作符接收來自上游釋出者的最新元素,並將它們結合到一個元組後進行傳送。
combineLatest
操作符要求上游釋出者的失敗型別完全相同,輸出型別可以不同。
combineLatest
操作符有多個版本,分別對應上游釋出者的個數:
- combineLatest
- combineLatest3
- combineLatest4
示例程式碼:
func combineLatestDemo() {
let oddPublisher = PassthroughSubject<Int, Never>()
let evenStringPublisher = PassthroughSubject<String, Never>()
oddPublisher
.combineLatest(evenStringPublisher)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
oddPublisher.send(1)
evenStringPublisher.send("2")
oddPublisher.send(3)
evenStringPublisher.send("4")
}
輸出內容:
(1, "2")
(3, "2")
(3, "4")
請注意,這裡的第一次輸出是 (1, "2")
,combineLatest
操作符的下游訂閱者只有在所有的上游釋出者都發布了值
之後才會收到結合了的值。
zip
官網文件:https://developer.apple.com/documentation/combine/publishers/zip
zip
操作符會將上游釋出者釋出的元素結合到一個流中,在每個上游釋出者傳送的元素配對時才向下遊傳送一個包含配對元素的元組。
zip
操作符要求上游釋出者的失敗型別完全相同,輸出型別可以不同。
zip
操作符有多個版本,分別對應上游釋出者的個數:
- zip
- zip3
- zip4
示例程式碼:
func zipDemo() {
let oddPublisher = PassthroughSubject<Int, Never>()
let evenStringPublisher = PassthroughSubject<String, Never>()
oddPublisher
.zip(evenStringPublisher)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
oddPublisher.send(1)
evenStringPublisher.send("2")
oddPublisher.send(3)
evenStringPublisher.send("4")
evenStringPublisher.send("6")
evenStringPublisher.send("8")
}
輸出內容:
(1, "2")
(3, "4")
請注意,因為 1 和 "2" 可以配對,3 和 "4" 可以配對,所以它們被輸出。而 "6" 和 "8" 無法完成配對,所以沒有被輸出。
和 combineLatest
操作符一樣,zip
操作符的下游訂閱者只有在所有的上游釋出者都發布了值
之後才會收到結合了的值。
setFailureType
官網文件:https://developer.apple.com/documentation/combine/publishers/setfailuretype
setFailureType
操作符可以將當前序列的失敗型別設定為指定的型別,主要用於適配具有不同失敗型別的釋出者。
示例程式碼:
func setFailureTypeDemo() {
let publisher = PassthroughSubject<Int, Error>()
Just(2)
.setFailureType(to: Error.self)
.merge(with: publisher)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
publisher.send(1)
}
輸出內容:
2
1
如果註釋 .setFailureType(to: Error.self)
這一行程式碼,編譯器就會給出錯誤:
Instance method 'merge(with:)' requires the types 'Never' and 'Error' be equivalent
因為,Just(2)
的失敗型別是 Never
,而 PassthroughSubject<Int, Error>()
的失敗型別是 Error
。
通過呼叫 setFailureType
操作符,可以將 Just(2)
的失敗型別設定為 Error
。
switchToLatest
官網文件:https://developer.apple.com/documentation/combine/publishers/switchtolatest
switchToLatest
操作符可以將來自多個釋出者的事件流展平為單個事件流。
switchToLatest
操作符可以為下游提供一個持續的訂閱流,同時內部可以切換多個釋出者。比如,對 Publisher<Publisher<Data, NSError>, Never>
型別呼叫 switchToLatest()
操作符後,結果會變成 Publisher<Data, NSError>
型別。下游訂閱者只會看到一個持續的事件流,即使這些事件可能來自於多個不同的上游釋出者。
下面是一個簡單的示例,可以讓我們更容易理解 switchToLatest
到底做了什麼。示例程式碼:
func switchToLatestDemo() {
let subjects = PassthroughSubject<PassthroughSubject<String, Never>, Never>()
subjects
.switchToLatest()
.sink(receiveValue: { print($0) })
.store(in: &cancellables)
let stringSubject1 = PassthroughSubject<String, Never>()
subjects.send(stringSubject1)
stringSubject1.send("A")
let stringSubject2 = PassthroughSubject<String, Never>()
subjects.send(stringSubject2) // 釋出者切換為 stringSubject2
stringSubject1.send("B") // 下游不會收到
stringSubject1.send("C") // 下游不會收到
stringSubject2.send("D")
stringSubject2.send("E")
stringSubject2.send(completion: .finished)
}
輸出內容:
A
D
E
下面將是一個更復雜但是卻更常見的用法,示例程式碼:
func switchToLatestDemo2() {
let subject = PassthroughSubject<String, Error>()
subject.map { value in
// 在這裡發起網路請求,或者其他可能失敗的任務
return Future<Int, Error> { promise in
if let intValue = Int(value) {
// 根據傳入的值來延遲執行
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(intValue)) {
print(#function, intValue)
promise(.success(intValue))
}
} else {
// 失敗就立刻完成
promise(.failure(Errors.notInteger))
}
}
.replaceError(with: 0) // 提供預設值,防止下游的訂閱因為失敗而被終止
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.switchToLatest()
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
subject.send("3") // 下游不會收到 3
subject.send("") // 立即失敗,下游會收到0,之前的 3 會被丟棄
subject.send("1") // 延時 1 秒後,下游收到 1
}
輸出內容:
0
switchToLatestDemo2() 1
1
switchToLatestDemo2() 3
請注意,在傳送了 ""
之後,之前傳送的 "3"
依然會觸發 Future
中的操作,但是這個 Future
裡的 promise(.success(intValue))
中傳入的 3
,下游不會收到。
總結
Combine
中還有非常多的預置操作符,如果您感興趣,可以去官網一探究竟:https://developer.apple.com/documentation/combine/publishers
雖然學習這些操作符的成本略高,但是當您掌握之後,開發效率必然會大幅提升。尤其是當 Combine
與 SwiftUI
以及 MVVM
結合在一起使用時,這些學習成本就會顯得更加值得!因為,它們可以幫助您寫出更簡潔、更易讀、更優雅,同時也更加容易測試的程式碼!
Ficow 還會繼續更新 Combine
系列的文章,後續的內容會講解如何將 Combine
與 SwiftUI
以及 MVVM
結合在一起使用。
推薦繼續閱讀:Combine 框架,從0到1 —— 5.Combine 中的 Scheduler
參考內容:
Using Combine
The Operators of ReactiveX
Combine — switchToLatest()