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 提供了大量内置操作符,如:
-
转换操作符:如
map、flatMap和scan,用于改变数据的形式或结构。-
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 */
-
-
过滤操作符:包括
filter、compactMap和removeDuplicates,用于选择性地处理某些数据。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 -
组合操作符:如
merge、zip和combineLatest,用于将多个数据流合并成一个。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 */ -
时间相关操作符:例如
debounce、throttle和delay,用于控制数据发送的时机。debounce:在指定时间窗口内,如果没有新的事件到达,才会发布最后一个事件。通常用于防止过于频繁的触发,比如搜索框的实时搜索。throttle:在指定时间间隔内,只发布一次。如果latest为true,会发布时间段内的最后一个元素,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 */ -
错误处理操作符:如
catch和retry,用于处理错误情况。 -
处理多个订阅者:例如
multicast和share-
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 */
share和multicast的区别:- 自动连接:使用
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
相关文章:
Swift Combine 学习(四):操作符 Operator
Swift Combine 学习(一):Combine 初印象Swift Combine 学习(二):发布者 PublisherSwift Combine 学习(三):Subscription和 SubscriberSwift Combine 学习(四&…...
leetcode 173.二叉搜索树迭代器栈绝妙思路
以上算法题中一个比较好的实现思路就是利用栈来进行实现,以下方法三就是利用栈来进行实现的,思路很好,很简练。进行next的时候,先是一直拿到左边的子树,直到null为止,这一步比较好思考一点,下一…...
df.groupby([pd.Grouper(freq=‘1M‘, key=‘Date‘), ‘Buyer‘]).sum()
df.groupby([pd.Grouper(freq1M, keyDate), Buyer]).sum() 用于根据特定的时间频率和买家(Buyer)对 DataFrame 进行分组,然后计算每个分组的总和。下面是对这行代码的逐步解释: df.groupby([...]):这个操作会根据传入的…...
LLM - 使用 LLaMA-Factory 部署大模型 HTTP 多模态服务 (4)
欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://spike.blog.csdn.net/article/details/144881432 大模型的 HTTP 服务,通过网络接口,提供 AI 模型功能的服务,允许通过发送 HTTP 请求,交互…...
icp备案网站个人备案与企业备案的区别
个人备案和企业备案是在进行ICP备案时需要考虑的两种不同情况。个人备案是指个人拥有的网站进行备案,而企业备案则是指企业或组织名下的网站进行备案。这两者在备案过程中有一些明显的区别。 首先,个人备案相对来说流程较为简单。个人备案只需要提供个人…...
如何不修改模型参数来强化大语言模型 (LLM) 能力?
前言 如果你对这篇文章感兴趣,可以点击「【访客必读 - 指引页】一文囊括主页内所有高质量博客」,查看完整博客分类与对应链接。 大语言模型 (Large Language Model, LLM, e.g. ChatGPT) 的参数量少则几十亿,多则上千亿,对其的训…...
AF3 AtomAttentionEncoder类的init_pair_repr方法解读
AlphaFold3 的 AtomAttentionEncoder 类中,init_pair_repr 方法方法负责为原子之间的关系计算成对表示(pair representation),这是原子转变器(atom transformer)模型的关键组成部分,直接影响对蛋白质/分子相互作用的建模。 init_pair_repr源代码: def init_pair_repr(…...
DDoS攻击防御方案大全
1. 引言 随着互联网的迅猛发展,DDoS(分布式拒绝服务)攻击成为了网络安全领域中最常见且危害严重的攻击方式之一。DDoS攻击通过向目标网络或服务发送大量流量,导致服务器过载,最终使其无法响应合法用户的请求。本文将深…...
Vue中常用指令
一、内容渲染指令 1.v-text:操作纯文本,用于更新标签包含的文本,但是使用不灵活,无法拼接字符串,会覆盖文本,可以简写为{{}},{{}}支持逻辑运算。 用法示例: //把name对应的值渲染到…...
Servlet解析
概念 Servlet是运行在服务端的小程序(Server Applet),可以处理客户端的请求并返回响应,主要用于构建动态的Web应用,是SpringMVC的基础。 生命周期 加载和初始化 默认在客户端第一次请求加载到容器中,通过反射实例化…...
带虚继承的类对象模型
文章目录 1、代码2、 单个虚继承3、vbptr是什么4、虚继承的多继承 1、代码 #include<iostream> using namespace std;class Base { public:int ma; };class Derive1 :virtual public Base { public:int mb; };class Derive2 :public Base { public:int mc; };class Deri…...
深度学习中的离群值
文章目录 深度学习中有离群值吗?深度学习中的离群值来源:处理离群值的策略:1. 数据预处理阶段:2. 数据增强和鲁棒模型:3. 模型训练阶段:4. 异常检测集成模型: 如何处理对抗样本?总结…...
如何利用Logo设计免费生成器创建专业级Logo
在当今的商业世界中,一个好的Logo是品牌身份的象征,它承载着公司的形象与理念。设计一个专业级的Logo不再需要花费大量的金钱和时间,尤其是当我们拥有Logo设计免费生成器这样的工具时。接下来,让我们深入探讨如何利用这些工具来创…...
Mysql SQL 超实用的7个日期算术运算实例(10k)
文章目录 前言1. 加上或减去若干天、若干月或若干年基本语法使用场景注意事项运用实例分析说明2. 确定两个日期相差多少天基本语法使用场景注意事项运用实例分析说明3. 确定两个日期之间有多少个工作日基本语法使用场景注意事项运用实例分析说明4. 确定两个日期相隔多少个月或多…...
运算指令(PLC)
加 ADD 减 SUB 乘 MUL 除 DIV 浮点运算 整数运算...
「Mac畅玩鸿蒙与硬件49」UI互动应用篇26 - 数字填色游戏
本篇教程将带你实现一个数字填色小游戏,通过简单的交互逻辑,学习如何使用鸿蒙开发组件创建趣味性强的应用。 关键词 UI互动应用数字填色动态交互逻辑判断游戏开发 一、功能说明 数字填色小游戏包含以下功能: 数字选择:用户点击…...
机器学习经典算法——逻辑回归
目录 算法介绍 算法概念 算法的优缺点 LogisticRegression()函数理解 环境准备 算法练习 算法介绍 算法概念 逻辑回归(Logistic Regression)是一种广泛应用于分类问题的机器学习算法。 它基于线性回归的思想,但通过引入一个逻辑函数&…...
【数据仓库金典面试题】—— 包含详细解答
大家好,我是摇光~,用大白话讲解所有你难懂的知识点 该篇面试题主要针对面试涉及到数据仓库的数据岗位。 以下都是经典的关于数据仓库的问题,希望对大家面试有用~ 1、什么是数据仓库?它与传统数据库有何区别? 数据仓库…...
【UE5 C++课程系列笔记】19——通过GConfig读写.ini文件
步骤 1. 新建一个Actor类,这里命名为“INIActor” 2. 新建一个配置文件“Test.ini” 添加一个自定义配置项 3. 接下来我们在“INIActor”类中获取并修改“CustomInt”的值。这里定义一个方法“GetINIVariable” 方法实现如下,其中第16行代码用于构建配…...
JS 中 json数据 与 base64、ArrayBuffer之间转换
JS 中 json数据 与 base64、ArrayBuffer之间转换 json 字符串进行 base64 编码 function jsonToBase64(json) {return Buffer.from(json).toString(base64); }base64 字符串转为 json 字符串 function base64ToJson(base64) {try {const binaryString atob(base64);const js…...
Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...
并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...
Python 实现 Web 静态服务器(HTTP 协议)
目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1)下载安装包2)配置环境变量3)安装镜像4)node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1)使用 http-server2)详解 …...
实战三:开发网页端界面完成黑白视频转为彩色视频
一、需求描述 设计一个简单的视频上色应用,用户可以通过网页界面上传黑白视频,系统会自动将其转换为彩色视频。整个过程对用户来说非常简单直观,不需要了解技术细节。 效果图 二、实现思路 总体思路: 用户通过Gradio界面上…...
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...
【C++】纯虚函数类外可以写实现吗?
1. 答案 先说答案,可以。 2.代码测试 .h头文件 #include <iostream> #include <string>// 抽象基类 class AbstractBase { public:AbstractBase() default;virtual ~AbstractBase() default; // 默认析构函数public:virtual int PureVirtualFunct…...
算法打卡第18天
从中序与后序遍历序列构造二叉树 (力扣106题) 给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。 示例 1: 输入:inorder [9,3,15,20,7…...
