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

news2025/1/5 8:20:50
  • 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 in
              a+b
          })
          .sink { print($0) }
      // 1 3 6 10
      
    • map:用于对上游发布者发出的值进行转换。它接收一个闭包,该闭包将每个从上游发布者接收到的值转换为新的值,然后将这个新值发给下游

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

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

    let numbers = ["1", "2", nil, "2", "4", "4", "5", "three", "6", "6", "6"]
    let publisher = numbers.publisher
    
    let 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 in
            print("Merge received: \(value)")
        }
    
    // 使用 zip 将 numberPublisher 和 letterPublisher 配对
    print("\n🍎Zip Example🍎")
    let zipSubscription = numberPublisher
        .zip(letterPublisher)
        .sink { number, letter in
            print("Zip received: number: \(number), letter: \(letter)")
        }
    
    // 使用 combineLatest 将 numberPublisher 和 letterPublisher 的最新值组合
    print("\n🍎CombineLatest Example🍎")
    let combineLatestSubscription = numberPublisher
        .combineLatest(letterPublisher)
        .sink { number, letter in
            print("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 SwiftUI
    
    class 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 in
                    print("Search request: \(text) at \(Date())")
                }.store(in: &cancellableSets!)
            
            // Simulate rapid input
            ["S", "Sw", "Swi", "Swif", "Swift"].enumerated().forEach { index, text in
                DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {
                    print("Input: \(text) at \(Date())")
                    searchText.send(text)
                }
            }
        }
        
        // Throttle Example
        func testThrottle() {
            print("🍎 Throttle Example 🍎")
            let scrollEvents = PassthroughSubject<Int, Never>()
            
            scrollEvents
                .throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: false)
                .sink { position in
                    print("Handle scroll position: \(position) at \(Date())")
                }
                .store(in: &cancellableSets!)
            
            // Simulate rapid scrolling
            (1...5).forEach { position in
                print("Scrolled to: \(position) at \(Date())")
                scrollEvents.send(position)
            }
        }
        
        // Delay Example
        func testDelay() {
            print("🍎 Delay Example 🍎")
            let notifications = PassthroughSubject<String, Never>()
            
            notifications
                .delay(for: .seconds(1), scheduler: DispatchQueue.main)
                .sink { message in
                    print("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 Combine
      
      var 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: 39
      
      With multicast():
      Subscriber 1 received: 89
      Subscriber 2 received: 89
      */
      
    • share:它是一个自动连接的多播操作符,会在第一个订阅者订阅时开始发送值,并且会保持对上游发布者的订阅直到最后一个订阅者取消订阅。当多个订阅者订阅时,所有订阅者接收相同的输出,而不是每次订阅时重新触发数据流。

      import Combine
      
      var 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: 62
      
      With 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.Failure
        let upstream: Upstream
        let transform: (Upstream.Output) -> Output
        
        func 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.coloradmin.cn/o/2269821.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

开源的go语言统一配置中心 - nacos + nacos go sdk

配置文件实时更新机制的场景需求 配置文件热更新主要应用于需要在不停机的情况下动态调整系统行为的场景&#xff0c;例如修改服务参数、切换数据源等。其原理在于通过一个中心化的管理平台来存储和分发最新的配置信息。当配置文件发生变化时&#xff0c;该平台会主动或被动地…

对45家“AI+安全”产品/方案的分析

一. 关键洞察 “AI+安全”创新非常活跃,一片百家争鸣之势,赛道选择上,以事件分诊Incident Triage、 安全辅助Security Copilots、自动化Automation三者为主为主,这充分反映了当前安全运营的主要需求,在产品理念选择上以 AI 和 自动化为主,这确实又切合上了在关键…

GESP202412 三级【数字替换】题解(AC)

》》》点我查看「视频」详解》》》 [GESP202412 三级] 数字替换 题目描述 小杨有一个包含 n n n 个数字的序列 A A A&#xff0c;即 A [ a 1 , a 2 , … , a n ] A[a_1,a_2,\ldots,a_n] A[a1​,a2​,…,an​]&#xff0c;他想将其中大于 k k k 的数字都替换为序列的最大…

springboot集成websokcet+H5开发聊天原型(二)

本文没有写完~~~~ 聊天相关数据结构&#xff1a; 我们初步设计了如下几个数据结构。 //存放 sessionId 与 userId 的map private Map<String,String> sessionId_userId new HashMap<>(); // 用于存储用户与群组的关联关系&#xff0c;键为用户ID&#xff0c;值…

List接口(源码阅读)

文章目录 1.List接口常用方法1.代码2.结果 2.ArrayList底层机制1.结论2.ArrayList底层源码1.代码2.debug添加第一个元素1.进入2.elementData数组存储ArrayList的数据3.初始化为空数组4.首先确保使用size1来计算最小容量5.如果elementData为空&#xff0c;最小容量就是106.modCo…

Python爬虫(一)- Requests 安装与基本使用教程

文章目录 前言一、简介及安装1. 简介2. 安装 Requests2.1 安装2.2 检查安装是否成功 二、使用 Requests 发送 HTTP 请求1. 发送 GET 请求2. 发送 POST 请求3. 发送 PUT 请求4. 发送 DELETE 请求5. 发送 HEAD 请求6. 发送 OPTIONS 请求 三、传递参数1. GET 请求传递 URL 参数1.1…

风力涡轮机缺陷检测数据集,86.6%准确识别率,11921张图片,支持yolo,PASICAL VOC XML,COCO JSON格式的标注

风力涡轮机缺陷检测数据集&#xff0c;86.6&#xff05;准确识别率&#xff0c;11921张图片&#xff0c;支持yolo&#xff0c;PASICAL VOC XML&#xff0c;COCO JSON格式的标注 数据集下载 yolov11&#xff1a; https://download.csdn.net/download/pbymw8iwm/90206849 yolov…

简易屏幕共享工具-基于WebSocket

前面写了两个简单的屏幕共享工具&#xff0c;不过那只是为了验证通过截屏的方式是否可行&#xff0c;因为通常手动截屏的频率很低&#xff0c;而对于视频来说它的帧率要求就很高了&#xff0c;至少要一秒30帧率左右。所以&#xff0c;经过实际的截屏工具验证&#xff0c;我了解…

python-leetcode-多数元素

169. 多数元素 - 力扣&#xff08;LeetCode&#xff09; class Solution:def majorityElement(self, nums: List[int]) -> int:candidate Nonecount 0for num in nums:if count 0: # 更新候选元素candidate numcount (1 if num candidate else -1)return candidate

js按日期按数量进行倒序排序,然后再新增一个字段,给这个字段赋值 10 到1

效果如下图&#xff1a; 实现思路&#xff1a; 汇总数据&#xff1a;使用 reduce 方法遍历原始数据数组&#xff0c;将相同日期的数据进行合并&#xff0c;并计算每个日期的总和。创建日期映射&#xff1a;创建一个映射 dateMap&#xff0c;存储每个日期的对象列表。排序并添加…

MM-2024 | 智能体遇山开路,遇水架桥! ObVLN:突破障碍,受阻环境中的视觉语言导航

作者&#xff1a;Haodong Hong, Sen Wang, Zi Huang 单位&#xff1a;昆士兰大学 论文链接&#xff1a;Navigating Beyond Instructions: Vision-and-Language Navigation in Obstructed Environments (https://dl.acm.org/doi/pdf/10.1145/3664647.3681640) 代码链接&#…

1Panel自建RustDesk服务器方案实现Windows远程macOS

文章目录 缘起RustDesk 基本信息实现原理中继服务器的配置建议 中继服务器自建指南准备服务器安装1Panel安装和配置 RustDesk 中继服务防火墙配置和安全组配置查看key下载&安装&配置客户端设置永久密码测试连接 macOS安装客户端提示finder写入失败hbbs和hbbr说明**hbbs…

Tube Qualify弯管测量系统在汽车管路三维检测中的应用

从使用量上来说&#xff0c;汽车行业是使用弯管零件数量最大的单一行业。在汽车的燃油&#xff0c;空调&#xff0c;排气&#xff0c;转向&#xff0c;制动等系统中都少不了管路。汽车管件形状复杂&#xff0c;且由于安装空间限制&#xff0c;汽车管件拥有不同弯曲半径&#xf…

Excel文件恢复教程:快速找回丢失数据!

Excel文件恢复位置在哪里&#xff1f; Excel是微软开发的电子表格软件&#xff0c;它为处理数据和组织工作提供了便捷。虽然数据丢失的问题在数字时代已经司空见惯&#xff0c;但对于某些用户来说&#xff0c;恢复未保存/删除/丢失的Excel文件可能会很困难&#xff0c;更不用说…

R语言入门笔记:第一节,快速了解R语言——文件与基础操作

关于 R 语言的简单介绍 上一期 R 语言入门笔记里面我简单介绍了 R 语言的安装和使用方法&#xff0c;以及各项避免踩坑的注意事项。我想把这个系列的笔记持续写下去。 这份笔记只是我的 R 语言入门学习笔记&#xff0c;而不是一套 R 语言教程。换句话说&#xff1a;这份笔记不…

16、【ubuntu】【gitlab】【补充】服务器断电后,重启服务器,gitlab无法访问

背景 接wiki 【服务器断电后&#xff0c;重启服务器&#xff0c;gitlab无法访问】https://blog.csdn.net/nobigdeal00/article/details/144280761 最近不小心把服务器重启&#xff0c;每次重启后&#xff0c;都会出现gitlab无法访问 分析 查看系统正在运行的任务 adminpcad…

汇编环境搭建

学习视频 将MASM所在目录 指定为C盘

两种分类代码:独热编码与标签编码

目录 一、说明 二、理解分类数据 2.1 分类数据的类型&#xff1a;名义数据与序数数据 2.2 为什么需要编码 三、什么是独热编码&#xff1f; 3.1 工作原理&#xff1a;独热编码背后的机制 3.2 应用&#xff1a;独热编码的优势 四、什么是标签编码&#xff1f; 4.1 工作原理&…

二、SQL语言,《数据库系统概念》,原书第7版

文章目录 一、概览SQL语言1.1 SQL 语言概述1.1.1 SQL语言的提出和发展1.1.2 SQL 语言的功能概述 1.2 利用SQL语言建立数据库1.2.1 示例1.2.2 SQL-DDL1.2.2.1 CREATE DATABASE1.2.2.2 CREATE TABLE 1.2.3 SQL-DML1.2.3.1 INSERT INTO 1.3 用SQL 语言进行简单查询1.3.1 单表查询 …

异常与中断(下)

文章目录 一、中断的硬件框架1.1 中断路径上的3个部件1.2 STM32F103的GPIO中断1.2.1 GPIO控制器1.2.2 EXTI1.2.3 NVIC1.2.4 CPU1. PRIMASK2. FAULTMASK3. BASEPRI 1.3 STM32MP157的GPIO中断1.3.1 GPIO控制器1.3.2 EXTI1. 设置EXTImux2. 设置Event Trigger3. 设置Masking4. 查看…