Swift Combine 学习(四):操作符 Operator

  • Swift Combine 学习(一):Combine 初印象
  • Swift Combine 学习(二):发布者 Publisher
  • Swift Combine 学习(三):Subscription和 Subscriber
  • Swift Combine 学习(四):操作符 Operator
  • Swift Combine 学习(五):Backpressure和 Scheduler
  • Swift Combine 学习(六):自定义 Publisher 和 Subscriber
  • Swift Combine 学习(七):实践应用场景举例

    文章目录

      • 引言
      • 操作符 (`Operator`)
      • 类型擦除(Type Erasure)
      • 结语

引言

在前几篇文章中,我们已经了解了 Combine 框架的基本概念、发布者和订阅者的工作机制。本文将详细介绍 Combine 中的操作符(Operator),这些操作符是处理和转换数据流的重要工具。通过学习各类操作符的使用,我们可以更灵活地处理异步事件流,构建复杂的数据处理链条,从而提升应用的响应能力和性能。

操作符 (Operator)

Operator 在 Combine 中用于处理、转换 Publisher 发出的数据。Operator 修改、过滤、组合或以其他方式操作数据流。Combine 提供了大量内置操作符,如:

  • 转换操作符:如 mapflatMapscan,用于改变数据的形式或结构。

    • scan:用于对上游发布者发出的值进行累加计算。它接收一个初始值和一个闭包,每次上游发布者发出一个新元素时,scan 会根据闭包计算新的累加值,并将累加结果传递给下游。

      let publisher = [1, 2, 3, 4].publisher
      publisher.scan(0, { a, b ina+b}).sink { print($0) }
      // 1 3 6 10
      
    • map:用于对上游发布者发出的值进行转换。它接收一个闭包,该闭包将每个从上游发布者接收到的值转换为新的值,然后将这个新值发给下游

      let nums = [1, 2, 3, 4, 5]
      let publisher = nums.publisherpublisher.map { $0 * 10 }  // 将每个数乘以10.sink { print($0)        }// 输出: 10 20 30 40 50
      
    • flatMap:用于将上游发布者发出的值转换为另一个发布者,并将新的发布者的值传递给下游。与 map 不同,它可以对发布者进行展平,消除嵌套。

      import Combinelet publisher = [[1, 2, 3], [4, 5, 6]].publisher// 使用 flatMap 将每个数组转换为新的发布者并展平
      let cancellable = publisher.flatMap { arr inarr.publisher // 将每个数组转换为一个新的发布者}.sink { value inprint(value)}/* 输出:
      1
      2
      3
      4
      5
      6
      */
      
  • 过滤操作符:包括 filtercompactMapremoveDuplicates,用于选择性地处理某些数据。

    let numbers = ["1", "2", nil, "2", "4", "4", "5", "three", "6", "6", "6"]
    let publisher = numbers.publisherlet subscription = publisher// 使用 compactMap 将字符串转换为整数。如果转换失败就过滤掉该元素.compactMap { $0.flatMap(Int.init) }// filter 过滤掉不符合条件的元素. 如过滤掉小于 3 的数.filter { $0 >= 3 }// 用 removeDuplicates 移除连续重复的元素.removeDuplicates().sink {print($0)}// 输出: 4 5 6
    
  • 组合操作符:如 mergezipcombineLatest,用于将多个数据流合并成一个。

    • combineLatest:用于将多个发布者的最新值合成一个新的发布者。每当任何一个输入发布者发出新值时,combineLatest 操作符会将每个发布者的最新值组合并作为元组向下游发送。
    • merge:用于将多个发布者合并为一个单一的发布者,以不确定性的顺序发出所有输入发布者的值。
    • zip:用于将两个发布者组合成一个新的发布者,该发布者发出包含每个输入发布者的最新值的元组。
    let numberPublisher = ["1", "2", nil].publisher.compactMap { Int($0 ?? "") }
    let letterPublisher = ["A", "B", "C"].publisher
    let extraNumberPublisher = ["10", "20", "30"].publisher.compactMap { Int($0) }// 使用 merge 合并 numberPublisher 和 extraNumberPublisher
    print("Merge Example:")
    let mergeSubscription = numberPublisher.merge(with: extraNumberPublisher).sink { value inprint("Merge received: \(value)")}// 使用 zip 将 numberPublisher 和 letterPublisher 配对
    print("\n🍎Zip Example🍎")
    let zipSubscription = numberPublisher.zip(letterPublisher).sink { number, letter inprint("Zip received: number: \(number), letter: \(letter)")}// 使用 combineLatest 将 numberPublisher 和 letterPublisher 的最新值组合
    print("\n🍎CombineLatest Example🍎")
    let combineLatestSubscription = numberPublisher.combineLatest(letterPublisher).sink { number, letter inprint("CombineLatest received: number: \(number), letter: \(letter)")}/*输出
    Merge Example:
    Merge received: 1
    Merge received: 3
    Merge received: 10
    Merge received: 20
    Merge received: 30🍎Zip Example🍎
    Zip received: number: 1, letter: A
    Zip received: number: 3, letter: B🍎CombineLatest Example🍎
    CombineLatest received: number: 3, letter: A
    CombineLatest received: number: 3, letter: B
    CombineLatest received: number: 3, letter: C
    */
    
  • 时间相关操作符:例如 debouncethrottledelay,用于控制数据发送的时机。

    • debounce:在指定时间窗口内,如果没有新的事件到达,才会发布最后一个事件。通常用于防止过于频繁的触发,比如搜索框的实时搜索。
    • throttle:在指定时间间隔内,只发布一次。如果 latesttrue,会发布时间段内的最后一个元素,false 时发布第一个元素。
    • delay:将事件的发布推迟指定时间。
    import UIKit
    import Combine
    import Foundation
    import SwiftUIclass ViewController: UIViewController {var cancellableSets: Set<AnyCancellable>?override func viewDidLoad() {super.viewDidLoad()cancellableSets = Set<AnyCancellable>()testDebounce()
    //        testThrottle()
    //        testDelay()}func testDebounce() {print("🍎 Debounce Example 🍎")let searchText = PassthroughSubject<String, Never>()searchText.debounce(for: .seconds(0.3), scheduler: DispatchQueue.main).sink { text inprint("Search request: \(text) at \(Date())")}.store(in: &cancellableSets!)// Simulate rapid input["S", "Sw", "Swi", "Swif", "Swift"].enumerated().forEach { index, text inDispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {print("Input: \(text) at \(Date())")searchText.send(text)}}}// Throttle Examplefunc testThrottle() {print("🍎 Throttle Example 🍎")let scrollEvents = PassthroughSubject<Int, Never>()scrollEvents.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: false).sink { position inprint("Handle scroll position: \(position) at \(Date())")}.store(in: &cancellableSets!)// Simulate rapid scrolling(1...5).forEach { position inprint("Scrolled to: \(position) at \(Date())")scrollEvents.send(position)}}// Delay Examplefunc testDelay() {print("🍎 Delay Example 🍎")let notifications = PassthroughSubject<String, Never>()notifications.delay(for: .seconds(1), scheduler: DispatchQueue.main).sink { message inprint("Display notification: \(message) at \(Date())")}.store(in: &cancellableSets!)print("Send notification: \(Date())")notifications.send("Operation completed")}
    }/*
    🍎 Debounce Example 🍎
    输入: S at 2024-10-21 09:23:19 +0000
    输入: Sw at 2024-10-21 09:23:19 +0000
    输入: Swi at 2024-10-21 09:23:19 +0000
    输入: Swif at 2024-10-21 09:23:19 +0000
    输入: Swift at 2024-10-21 09:23:19 +0000
    搜索请求: Swift at 2024-10-21 09:23:19 +0000
    */
    
  • 错误处理操作符:如 catchretry,用于处理错误情况。

  • 处理多个订阅者:例如 multicastshare

    • multicast:使用 multicast 操作符时,它会将原始的 Publisher 包装成一个ConnectablePublisher,并且将所有订阅者的订阅合并为一个单一的订阅。这样,无论有多少个订阅者,原始的 Publisher 都只会收到一次 receive(_:) 调用,即对每个事件只处理一次。然后,multicast 操作符会将事件分发给所有的订阅者。

      import Combinevar cancelables: Set<AnyCancellable> = Set<AnyCancellable>()let publisher = PassthroughSubject<Int, Never>()// 不使用 multicast() 的情况
      let randomPublisher1 = publisher.map { _ in Int.random(in: 1...100) }print("Without multicast():")
      randomPublisher1.sink {print("Subscriber 1 received: \($0)")}.store(in: &cancelables)randomPublisher1.sink {print("Subscriber 2 received: \($0)")}.store(in: &cancelables)publisher.send(1)let publisher2 = PassthroughSubject<Int, Never>()// 使用 multicast() 的情况
      let randomPublisher2 = publisher2.map { _ in Int.random(in: 1...100) }.multicast(subject: PassthroughSubject<Int, Never>())print("\nWith multicast():")
      randomPublisher2.sink {print("Subscriber 1 received: \($0)")}.store(in: &cancelables)randomPublisher2.sink {print("Subscriber 2 received: \($0)")}.store(in: &cancelables)let connect = randomPublisher2.connect()
      publisher2.send(1)/*输出:
      Without multicast():
      Subscriber 1 received: 43
      Subscriber 2 received: 39With multicast():
      Subscriber 1 received: 89
      Subscriber 2 received: 89
      */
      
    • share:它是一个自动连接的多播操作符,会在第一个订阅者订阅时开始发送值,并且会保持对上游发布者的订阅直到最后一个订阅者取消订阅。当多个订阅者订阅时,所有订阅者接收相同的输出,而不是每次订阅时重新触发数据流。

      import Combinevar cancellables: Set<AnyCancellable> = Set<AnyCancellable>()let publisher = PassthroughSubject<Int, Never>()// 不使用 share() 的情况
      let randomPublisher1 = publisher.map { _ in Int.random(in: 1...100)}print("Without share():")
      randomPublisher1.sink {print("Subscriber 1 received: \($0)")}.store(in: &cancellables)randomPublisher1.sink {print("Subscriber 2 received: \($0)")}.store(in: &cancellables)publisher.send(1)let publisher2 = PassthroughSubject<Int, Never>()// 使用 share() 的情况
      let randomPublisher2 = publisher2.map { _ in Int.random(in: 1...100)}.share()print("\nWith share():")
      randomPublisher2.sink {print("Subscriber 1 received: \($0)")}.store(in: &cancellables)randomPublisher2.sink {print("Subscriber 2 received: \($0)")}.store(in: &cancellables)publisher2.send(1)/*
      输出
      Without share():
      Subscriber 2 received: 61
      Subscriber 1 received: 62With share():
      Subscriber 2 received: 92
      Subscriber 1 received: 92
      */
      

    sharemulticast 的区别:

    • 自动连接:使用 share 时,原始 Publisher 会在第一个订阅者订阅时自动连接,并在最后一个订阅者取消订阅时自动断开连接。
    • 无需手动连接:无需显式调用 connect() 方法来启动数据流,share 会自动管理连接。

我们可以使用这些操作符创建成一个链条。Operator 通常作为 Publisher 的扩展方法实现。

以下是一个简化的 map 操作符示例:

extension Publishers {struct Map<Upstream: Publisher, Output>: Publisher {typealias Failure = Upstream.Failurelet upstream: Upstreamlet transform: (Upstream.Output) -> Outputfunc receive<S: Subscriber>(subscriber: S) where S.Input == Output, S.Failure == Failure {upstream.subscribe(Subscriber(downstream: subscriber, transform: transform))}}
}extension Publisher {func map<T>(_ transform: @escaping (Output) -> T) -> Publishers.Map<Self, T> {return Publishers.Map(upstream: self, transform: transform)}
}

类型擦除(Type Erasure)

类型擦除(type erasure)允许在不暴露具体类型的情况下,对遵循相同协议的多个类型进行统一处理。换句话说,类型擦除可以将不同类型的数据包装成一个统一的类型,从而实现更灵活、清晰、通用的编程。

let publisher = Just(5).map { $0 * 2 }.filter { $0 > 5 }

在这个简单的例子中 Publisher 的实际类型是 Publishers.Filter<Publishers.Map<Just<Int>, Int>, Int>。类型会变得非常复杂,特别是在使用多个操作符连接多个 Publisher 的时候。回到 Combine 中的 AnySubscriber 和 AnyPublisher,每个 Publisher 都有一个方法 eraseToAnyPublisher(),它可以返回一个 AnyPublisher 实例。就会被简化为 AnyPublisher<Int, Never>

let publisher: AnyPublisher<Int, Never> = Just(5).map { $0 * 2 }.filter { $0 > 5 }.eraseToAnyPublisher()  // 使用 eraseToAnyPublisher 方法对 Publisher 进行类型擦除

因为是 Combine 的学习,在此不对类型擦除展开过多。

结语

操作符是 Combine 框架中强大的工具,它们使得数据流的处理和转换变得更加灵活和高效。通过掌握操作符的使用,开发者可以创建更复杂和功能强大的数据处理逻辑。在下一篇文章中,我们将深入探讨 Combine 中的 Backpressure 和 Scheduler,进一步提升对异步数据流的理解和控制调度能力。

  • Swift Combine 学习(五):Backpressure和 Scheduler

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/501385.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C语言】_指针运算

目录 1. 指针-整数 2. 指针-指针 2.1 指针-指针含义 2.2 指针-指针运算应用&#xff1a;实现my_strlen函数 3. 指针的关系运算&#xff08;大小比较&#xff09; 1. 指针-整数 联系关于指针变量类型关于指针类型和指针-整数相关知识&#xff1a; 原文链接如下&#xff1…

Wend看源码-Java-Executor异步执行器学习

摘要 本文主要介绍了Java.util.concurrent包所提供的 Executor 异步执行器框架&#xff0c;涵盖了相关的接口和类。 并发执行器类图 图1 java 并发执行器相关类图 Executor 接口 Executor 接口提供了一种将任务的提交与任务的实际执行机制分离开来的方法。它只有一个方法 exe…

2025考研江南大学复试科目控制综合(初试807自动控制原理)

​ 2025年全国硕士研究生招生考试江南大学考点 一年年的考研如期而至&#xff0c;我也变成了研二了&#xff0c;作为2次考研经历的学长&#xff0c;总是情不自禁地回想起自己的考研经历&#xff0c;我也会经常从那段经历中汲取力量。我能理解大多数考生考完后的的迷茫无助&…

基于深度学习算法的AI图像视觉检测

基于人工智能和深度学习方法的现代计算机视觉技术在过去10年里取得了显著进展。如今&#xff0c;它被广泛用于图像分类、人脸识别、图像中物体的识别等。那么什么是深度学习&#xff1f;深度学习是如何应用在视觉检测上的呢&#xff1f; 什么是深度学习&#xff1f; 深度学习是…

lec5-传输层原理与技术

lec5-传输层原理与技术 1. 传输层概述 1.1. 关键职责 flow control&#xff0c;流量控制reliability&#xff0c;可靠性 1.2. TCP与UDP对比 面向连接 / 不能连接对数据校验 / 不校验数据丢失重传 / 不会重传有确认机制 / 没有确认滑动窗口流量控制 / 不会流量控制 1.3. 关…

学习C++:数组

数组&#xff1a; 一&#xff0c;概述 所谓数组&#xff0c;就是一个集合&#xff0c;里面存放了相同类型的元素 特点1&#xff1a;数组中的每个数据元素都是相同的数据类型 特点2&#xff1a;数组是由连续的内存位置组成的 二&#xff0c;一维数组 1.一维数组定义方式 三…

Formality:官方Tutorial(一)

相关阅读 Formalityhttps://blog.csdn.net/weixin_45791458/category_12841971.html?spm1001.2014.3001.5482 本文是对Synopsys Formality User Guide Tutorial中第一个实验的翻译&#xff08;有删改&#xff09;&#xff0c;Lab文件可以从以下链接获取。 Formality官方Tu…

STM32 拓展 RTC(实时时钟)

RTC简介 RTC(Real Time Clock,实时时钟)。是一个掉电后仍然可以继续运行的独立定时器。 RTC模块拥有一个连续计数的计数器,在相应的软件配置下,可以提供时钟日历的功能。修改计数器的值可以重新设置当前时间和日期 RTC还包含用于管理低功耗模式的自动唤醒单元。 RTC实质…

在 macOS 上,你可以使用系统自带的 终端(Terminal) 工具,通过 SSH 协议远程连接服务器

文章目录 1. 打开终端2. 使用 SSH 命令连接服务器3. 输入密码4. 连接成功5. 使用密钥登录&#xff08;可选&#xff09;6. 退出 SSH 连接7. 其他常用 SSH 选项8. 常见问题排查问题 1&#xff1a;连接超时问题 2&#xff1a;权限被拒绝&#xff08;Permission denied&#xff09…

Scrum中敏捷项目经理(Scrum Master)扮演什么角色?

敏捷开发模式已经逐渐被主流的软件研发团队所接受&#xff0c;其中Scrum是最具代表性的敏捷方法之一。Scrum框架中有三个核心角色&#xff1a;Product Owner&#xff08;PO&#xff09;、Scrum Master&#xff08;SM&#xff09;和Development Team&#xff08;DT&#xff09;。…

沙箱模拟支付宝支付3--支付的实现

1 支付流程实现 演示案例 主要参考程序员青戈的视频【支付宝沙箱支付快速集成版】支付宝沙箱支付快速集成版_哔哩哔哩_bilibili 对应的源码在 alipay-demo: 使用支付宝沙箱实现支付功能 - Gitee.com 以下是完整的实现步骤 1.首先导入相关的依赖 <?xml version"1…

Yocto项目 - 详解PACKAGECONFIG机制

引言 Yocto项目是一个强大的嵌入式Linux开发工具&#xff0c;广泛应用于创建定制的嵌入式Linux发行版。在Yocto中&#xff0c;配置和定制化构建系统、软件包、以及生成适用于特定硬件的平台镜像是非常重要的。PACKAGECONFIG是Yocto项目中用于灵活启用或禁用软件包特性的强大工…

【STM32】项目实战——OV7725/OV2604摄像头颜色识别检测(开源)

本篇文章分享关于如何使用STM32单片机对彩色摄像头&#xff08;OV7725/OV2604&#xff09;采集的图像数据进行分析处理&#xff0c;最后实现颜色的识别和检测。 目录 一、什么是颜色识别 1、图像采集识别的一些基本概念 1. 像素&#xff08;Pixel&#xff09; 2. 分辨率&am…

安装PyQt5-tools卡在Preparing metadata (pyproject.toml)解决办法

为了在VS code中使用PyQt&#xff0c;在安装PyQt5-tools时总卡在如下这一步 pyqt5 Preparing metadata (pyproject.toml)经过各种尝试&#xff0c;最终问题解决&#xff0c;在此记录方法。 首先进入PyQt5-tools官网查看其适配的Python版本&#xff0c;网址如下&#xff1a; h…

RAG实战:本地部署ragflow+ollama(linux)

1.部署ragflow 1.1安装配置docker 因为ragflow需要诸如elasticsearch、mysql、redis等一系列三方依赖&#xff0c;所以用docker是最简便的方法。 docker安装可参考Linux安装Docker完整教程&#xff0c;安装后修改docker配置如下&#xff1a; vim /etc/docker/daemon.json {…

56.在 Vue 3 中使用 OpenLayers 通过 moveend 事件获取地图左上和右下的坐标信息

前言 在现代 Web 开发中&#xff0c;地图应用越来越成为重要的组成部分。OpenLayers 是一个功能强大的 JavaScript 地图库&#xff0c;它提供了丰富的地图交互和操作功能&#xff0c;而 Vue 3 是当前流行的前端框架之一。在本篇文章中&#xff0c;我们将介绍如何在 Vue 3 中集…

Codigger集成Copilot:智能编程助手

在信息技术的快速发展中&#xff0c;编程效率和创新能力的提升成为了开发者们追求的目标。Codigger平台通过集成Copilot智能编程助手&#xff0c;为开发者提供了一个强大的工具&#xff0c;以增强其生产力、创新力和技能水平。本文将深入探讨Codigger与Copilot的集成如何为IT专…

用uniapp写一个播放视频首页页面代码

效果如下图所示 首页有导航栏&#xff0c;搜索框&#xff0c;和视频列表&#xff0c; 导航栏如下图 搜索框如下图 视频列表如下图 文件目录 视频首页页面代码如下 <template> <view class"video-home"> <!-- 搜索栏 --> <view class…

Java高频面试之SE-08

hello啊&#xff0c;各位观众姥爷们&#xff01;&#xff01;&#xff01;本牛马baby今天又来了&#xff01;哈哈哈哈哈嗝&#x1f436; 成员变量和局部变量的区别有哪些&#xff1f; 在 Java 中&#xff0c;成员变量和局部变量是两种不同类型的变量&#xff0c;它们在作用域…

在Typora中实现自动编号

文章目录 在Typora中实现自动编号1. 引言2. 准备工作3. 自动编号的实现3.1 文章大纲自动编号3.2 主题目录&#xff08;TOC&#xff09;自动编号3.3 文章内容自动编号3.4 完整代码 4. 应用自定义CSS5. 结论 在Typora中实现自动编号 1. 引言 Typora是一款非常流行的Markdown编辑…