Combine Publisher詳解

摘自《SwiftUI和Combine編程》---《Publisher 和常見 Operator》

Publisher 詳解

Publisher 在接收到訂閱促王,并且接受到請求要求提供若干個事件后鳖擒,才開始對外發(fā)布事件暑脆。接受訂閱和接受請求,也是 Publisher 生命周期的一部分呵扛。

Empty

Empty<Int, SampleError>()

是一個最簡單的 Publisher狡门,它只在被訂閱的時候發(fā)布一個完成事件 (receive finished)舞竿。這個 publisher 不會輸出任何的 output 值,只能用于表示某個事件已經(jīng)發(fā)生奉芦。

/// A publisher that never publishes any values, and optionally finishes immediately.
///
/// You can create a ”Never” publisher — one which never sends values and never finishes or fails — with the initializer `Empty(completeImmediately: false)`.
public struct Empty<Output, Failure> : Publisher, Equatable where Failure : Error

Just

Just(1)

表示一個單一的值赵抢,在被訂閱后,這個值會被發(fā)送出去声功,緊接著是 finished

/// A publisher that emits an output to each subscriber just once, and then finishes.
///
/// You can use a ``Just`` publisher to start a chain of publishers. A ``Just`` publisher is also useful when replacing a value with ``Publishers/Catch``.
public struct Just<Output> : Publisher

Publisher.Sequence

如果我們對一連串的值感興趣的話烦却,可以使用的是 Publishers.Sequence。顧名思義先巴,Publishers.Sequence 接受一個 Sequence:它可以是一個數(shù)組其爵,也可以是一個 Range。在被訂閱時伸蚯,Sequence 中的元素被逐個發(fā)送出來

Publisher.Sequence<[Int], Never>(sequence: [1,2,3])
//等價于
[1,2,3].publisher

常見 Operator

map

元素變形:map 操作可以為我們完成事件中數(shù)據(jù)類型的轉(zhuǎn)換

[1,2,3].publisher.map { $0*2 }

compactMap

將 map 結(jié)果中那些 nil 的元素去除掉醋闭,這個操作通常會“壓縮”結(jié)果,讓其中的元素數(shù)減少朝卒,這也正是其名字中 compact 的來源

flatMap

展平 降維

map 及 compactMap 的閉包返回值是單個的 Output 值证逻。而與它們不同,flatMap 的變形閉包里需要返回一個 Publisher。也就是說囚企,flatMap 將會涉及兩個 Publisher:一個是 flatMap 操作本身所作用的外層 Publisher丈咐,一個是 flatMap 所接受的變形閉包中返回的內(nèi)層 Publisher。flatMap 將外層 Publisher 發(fā)出的事件中的值傳遞給內(nèi)層 Publisher龙宏,然后匯總內(nèi)層 Publisher 給出的事件輸出棵逊,作為最終變形后的結(jié)果。

check("Flat Map 1") {
    [[1, 2, 3], ["a", "b", "c"]]
        .publisher
        .flatMap {
            $0.publisher
        }
}
// Output:
// 1 2 3 a b c

/**
在被訂閱后银酗,這個外層 Publisher 會發(fā)送兩個 Output 事件 (兩個事件的值分別是 [1, 2, 3] 和 ["a", "b", "c"])辆影,每個事件的值被 flatMap 傳遞到內(nèi)層,并通過 $0.publisher 生成新的 Publisher 并返回黍特。內(nèi)層 Publisher 實際上是 [1, 2, 3].publisher 和 ["a", "b", "c"].publisher蛙讥,它們發(fā)送的值將被作為 flatMap 的結(jié)果,被“展平” (flatten) 后發(fā)送出去灭衷。
*/
check("Flat Map 2") {
    ["A", "B", "C"]
        .publisher
        .flatMap { letter in
            [1, 2, 3]
                .publisher
                .map { "\(letter)\($0)" }
        }
}
// Output:
// A1 A2 A3 B1 B2 B3 C1 C2 C3

/**
外層 ["A", "B", "C"] 里的每個元素次慢,作為 flatMap 變形閉包的輸入,參與到了內(nèi)層 Publisher 的 map 計算翔曲。內(nèi)層 Publisher 逐次使用 [1, 2, 3] 里的元素迫像,和輸入進來的 letter (也就是 “A”, “B” 和 “C”) 進行拼接后作為新事件發(fā)出。
*/

reduce

操作完發(fā)消息

將數(shù)組中的元素按照某種規(guī)則進行合并瞳遍,并得到一個最終的結(jié)果闻妓。

與 scan 區(qū)別:當序列中值耗盡時,它將發(fā)布 finished掠械。而經(jīng)過 reduce 變形后由缆,新的 Publisher 只會在接到上游發(fā)出的 finished 事件后,才會將 reduce 后的結(jié)果發(fā)布出來份蝴。而緊接這個結(jié)果犁功,則是新的 reduce Publisher 的結(jié)束事件。

scan

每一步都發(fā)消息

場景:在某個下載任務(wù)執(zhí)行期間婚夫,接受 URLSession 的數(shù)據(jù)回調(diào)浸卦,將已接收到的數(shù)據(jù)量做累加來提供一個下載進度條的界面。

同 reduce案糙,但是會記錄中途每一步的過程限嫌。

與 reduce 區(qū)別:一邊進行重復(fù)操作,一邊將每一步中間狀態(tài)發(fā)送出去时捌。


源碼類似: 可直接添加到 Sequence extension 中

extension Sequence {
    public func scan<ResultElement>(_ initial: ResultElement, _ nextPartialResult: (ResultElement, Element) -> ResultElement) -> [ResultElement] {
        var result: [ResultElement] = []
        for x in self {
            result.append(nextPartialResult(result.last ?? initial, x))
        }
        return result
    }
}

removeDuplicates

移除連續(xù)出現(xiàn)的重復(fù)事件值

removeDuplicates 經(jīng)常被用來減少那些非常消耗資源的操作怒医,比如由事件觸發(fā)造成的網(wǎng)絡(luò)請求或者圖片渲染。如果當作為源頭的數(shù)據(jù)沒有改變時奢讨,所預(yù)期得到的結(jié)果也不會變化的話稚叹,那么就沒有必要去重復(fù)這樣操作。在源頭將重復(fù)的事件移除,可以讓下游的事件流也變得簡單扒袖。

check("Remove Duplicates") {
    // removeDuplicates 會處理事件源
    ["S", "Sw", "Sw", "Sw", "Swi",
     "Swif", "Swift", "Swift", "S"] 
        .publisher
        .removeDuplicates()
}
// Output:
// subscription: (["S", "Sw", "Swi", "Swif", "Swift", "S"])

錯誤處理

Fail

Fail 這個內(nèi)建的基礎(chǔ) Publisher塞茅,它所做的事情就是在被訂閱時發(fā)送一個錯誤事件.

Fail<Int, SampleError>(error: .sampleError)

public enum SampleError: Error {
    case sampleError
}

mapError

如果 Publisher 在出錯時發(fā)送的是 SampleError,但訂閱方聲明只接受 MyError 時季率,就算實際上 Publisher 只發(fā)出 Output 值而從不會發(fā)出 Failure 值野瘦,我們也無法使用這個 Subscriber 去接收一個類型不符的 Publisher 的事件。

在這種情況下飒泻,我們可以通過使用 mapError 來將 Publisher 的 Failure 轉(zhuǎn)換成 Subscriber 所需要的 Failure 類型

Fail<Int, SampleError>(error: .sampleError)
    .mapError { _ in
        myError.myError
    }

拋出錯誤

Combine 為 Publisher 的 map 操作提供了一個可以拋出錯誤的版本鞭光,tryMap。使用 tryMap 我們就可以將這類處理數(shù)據(jù)時發(fā)生的錯誤轉(zhuǎn)變?yōu)闃酥臼录魇〉慕Y(jié)束事件泞遗。

["1", "2", "S", "4"].publisher
    .tryMap { s -> Int in
        guard let value = Int(s) else {
            throw MyError.myError
        }
        return value
    }

除了 tryMap 以外惰许,Combine 中還有很多類似的以 try 開頭的 Operator,比如 tryScan刹孔,tryFilter啡省,tryReduce 等等娜睛。當你有需求在數(shù)據(jù)轉(zhuǎn)換或者處理時髓霞,將事件流以錯誤進行終止,都可以使用對應(yīng)操作的 try 版本來進行拋出畦戒,并在訂閱者一側(cè)接收到對應(yīng)的錯誤事件方库。

從錯誤中恢復(fù)

如果我們想要在事件流以錯誤結(jié)束時被轉(zhuǎn)為一個默認值的話,replaceError 就會很有用障斋。

replaceError 會將 Publisher 的 Failure 類型抹為 Never纵潦,這正是我們使用 assign 來將 Publisher 綁定到 UI 上時所需要的 Failure 類型。我們可以用 replaceError 來提供這樣一個在出現(xiàn)錯誤時應(yīng)該顯示的默認值垃环。

有一些 Operator 是專門幫助事件流從錯誤中恢復(fù)的邀层,最簡單的是 replaceError,它會把錯誤替換成一個給定的值遂庄,并且立即發(fā)送 finished 事件

replaceError(中斷信號)

["1", "2", "Swift", "4"].publisher
    .tryMap { s -> Int in
        guard let value = Int(s) else {
            throw MyError.myError
        }
        return value
    }
    .replaceError(with: -1)
// Output:
// 1 2 -1

catch(中斷信號)

catch 則略有不同寥院,它接受的是一個新的 Publisher,當上游 Publisher 發(fā)生錯誤時涛目,catch 操作會使用新的 Publisher 來把原來的 Publisher 替換掉秸谢。

["1", "2", "Swift", "4"].publisher
    .tryMap { s -> Int in
        guard let value = Int(s) else {
            throw MyError.myError
        }
        return value
    }
    .catch { _ in [-1, -2, -3].publisher }

 // Output:
 // 1 2 -1 -2 -3
 
 // 當錯誤發(fā)生后,原本的 Publisher 事件流將被中斷霹肝,取而代之估蹄,則是由 catch 所提供的事件流繼續(xù)向后續(xù)的 Operator 及 Subscriber 發(fā)送事件。原來 Publisher 中的最后一個元素 “4”沫换,將沒有機會到達臭蚁。

組合實現(xiàn)繼續(xù)輸入

如果我們將 (由 ["1", "2", "Swift", "4"] 構(gòu)成的) 原 Publisher 看作是用戶輸入,將結(jié)果的 Int 看作是最后輸出,那么像上面那樣的方式使用 replaceError 或者 catch 的話垮兑,一旦用戶輸入了不能轉(zhuǎn)為 Int 的非法值 (如 “Swift”)炭晒,整個結(jié)果將永遠停在我們給定的默認恢復(fù)值上,接下來的任意用戶輸入都將被完全忽略甥角。這往往不是我們想要的結(jié)果网严,一般情況下,我們會想要后續(xù)的用戶輸入也能繼續(xù)驅(qū)動輸出嗤无,這時候我們可以靠組合一些 Operator 來完成所需的邏輯

check("Catch and Continue") {
    ["1", "2", "Swift", "4"].publisher
        .flatMap { s in
            return Just(s)
                .tryMap { s -> Int in
                    guard let value = Int(s) else {
                        throw MyError.myError
                    }
                    return value
                }
                .catch { _ in
                    Just(-1)}
        }
}

// Output:
// 1 2 -1 4

eraseToAnyPublisher

類型抹消

let a = [[1,2,3],[4,5,6]].publisher.flatMap{ $0.publisher }

// 等價于
let a1 = Publishers.FlatMap(upstream: [[1,2,3],[4,5,6]].publisher, maxPublishers: .unlimited) {
    $0.publisher
}

let b = a.map {$0*2}
// 此時 b 的類型為: Publishers.Map<Publishers.FlatMap<Publishers.Sequence<[Int], Never>, Publishers.Sequence<[[Int]], Never>>, Int>

Combine 提供了 eraseToAnyPublisher 來幫助我們對復(fù)雜類型的 Publisher 進行類型抹消震束,這個方法返回一個 AnyPublisher。

let c = b.eraseToAnyPublisher()
// c: AnyPublisher<Int, Never>

Combine 中的其他角色也大都提供了類似的抹消后的類型当犯,比如 AnySubscriber 和 AnySubject 等垢村。在大多數(shù)情況下我們都只會關(guān)注某個部件所扮演的角色,也即嚎卫,它到底是一個 Publisher 還是一個 Subscriber嘉栓。一般我們并不關(guān)心具體的類型,因為對 Publisher 的變形往往都伴隨著類型的變化拓诸。通過類型抹消侵佃,可以讓事件的傳遞和訂閱操作變得更加簡單,對外的 API 也更加穩(wěn)定奠支。

操作符熔合

將操作符的作用時機提前到創(chuàng)建 Publisher 時的方式馋辈,被稱為操作符熔合 (operator fusion)。

有時候 map 操作的返回結(jié)果的類型并不是 Publishers.Map倍谜,比如下面的兩個例子:

[1, 2, 3].publisher.map { $0 * 2 }
// Publishers.Sequence<[Int], Never>
Just(10).map { String($0) }
// Just<String>

這是由于 Publishers.Sequence 和 Just 在各自的擴展中對默認的 Publisher 的 map 操作進行了重寫迈螟。由于 Publishers.Sequence 和 Just 這樣的類型在編譯期間我們就能確定它們在被訂閱時就會同步地發(fā)送所有事件,所以可以將 map 的操作直接作用在輸入上尔崔,而不需要等待每次事件發(fā)生時再去操作答毫。

可以想象 Publishers.Sequence 上 map 操作的實現(xiàn):

extension Publishers.Sequence {
public func map<T>(_ transform: (Elements.Element) -> T) -> Publishers.Sequence<[T], Failure> {
    return Publishers.Sequence(sequence: sequence.map(transform))
    }
}

這樣做可以避免 transform 的 @escaping 的要求,避免存儲這個變形閉包季春,讓所得到的 Publisher 更加高效洗搂,同時也讓后續(xù) Publisher 變形類型能簡單一些。這些優(yōu)點在多次變形的時候尤為突出鹤盒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚕脏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子侦锯,更是在濱河造成了極大的恐慌驼鞭,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尺碰,死亡現(xiàn)場離奇詭異挣棕,居然都是意外死亡译隘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門洛心,熙熙樓的掌柜王于貴愁眉苦臉地迎上來固耘,“玉大人,你說我怎么就攤上這事词身√浚” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵法严,是天一觀的道長损敷。 經(jīng)常有香客問我,道長深啤,這世上最難降的妖魔是什么拗馒? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮溯街,結(jié)果婚禮上诱桂,老公的妹妹穿的比我還像新娘。我一直安慰自己呈昔,他們只是感情好挥等,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著韩肝,像睡著了一般触菜。 火紅的嫁衣襯著肌膚如雪九榔。 梳的紋絲不亂的頭發(fā)上哀峻,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音哲泊,去河邊找鬼剩蟀。 笑死,一個胖子當著我的面吹牛切威,可吹牛的內(nèi)容都是我干的育特。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼先朦,長吁一口氣:“原來是場噩夢啊……” “哼缰冤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喳魏,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤棉浸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后刺彩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體迷郑,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡枝恋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了嗡害。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片焚碌。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖霸妹,靈堂內(nèi)的尸體忽然破棺而出十电,到底是詐尸還是另有隱情,我是刑警寧澤叹螟,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布摆出,位于F島的核電站,受9級特大地震影響首妖,放射性物質(zhì)發(fā)生泄漏偎漫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一有缆、第九天 我趴在偏房一處隱蔽的房頂上張望象踊。 院中可真熱鬧,春花似錦棚壁、人聲如沸杯矩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽史隆。三九已至,卻和暖如春曼验,著一層夾襖步出監(jiān)牢的瞬間泌射,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工鬓照, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留熔酷,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓豺裆,卻偏偏與公主長得像拒秘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子臭猜,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容