当前位置:首页 > 技术 > 正文内容

Combine 框架详解:发布者、订阅者与操作符

访客 技术 2026年6月10日 1

Combine 是 Apple 推出的一个声明式函数响应式编程框架,用于处理事件流和异步操作。

核心概念:Publisher, Operator, Subscriber

Combine 的核心围绕三个主要协议展开:Publisher、Operator 和 Subscriber。

Publisher (发布者)

Publisher 负责生成并发布数据。它是一个协议,其类型由发布的数据(Output)和可能发生的错误(Failure)共同定义,表示为 Publisher。发布者可以发出零个、一个或多个数据,并最终以正常完成或出错的方式结束。

Operator (操作符)

Operator 用于响应和转换数据流。操作符接收特定类型的数据和错误作为输入 (Input, Failure),并产生另一组类型的数据和错误作为输出 (Output, Failure)。操作符可以链式调用,对数据流进行一系列的加工和处理。

Subscriber (订阅者)

Subscriber 用于接收和处理来自发布者的数据。它是一个协议,其类型由接收的数据(Input)和可能发生的错误(Failure)定义,表示为 Subscriber。订阅者通过 subscribe(_:) 方法与发布者建立连接。

一个典型的 Combine 数据流示例如下:


// Publisher: Just 发出单个值
Just(10)
    // Operator: map 将整数转换为字符串
    .map { String($0) }
    // Subscriber: sink 处理最终接收到的值
    .sink { receivedString in
        print("最终结果是: \(receivedString)")
    }

Subject (主题)

Subject 是一种特殊的 Publisher,它本身也遵循 Publisher 协议,并额外提供了一个 send(_:) 方法来主动发送数据。Combine 提供了两个常用的 Subject 实现:PassthroughSubjectCurrentValueSubject

  • PassthroughSubject: 这种 Subject 没有初始值,也不会存储当前状态。订阅者只有在订阅之后才能接收到它发送的数据。可以将其比作一个门铃,没有固定的状态,只有当有人按门铃(发送数据)时,在家的人(订阅者)才能听到。
  • CurrentValueSubject: 这种 Subject 包含一个初始值,并始终保持当前状态。订阅者在订阅时会立即收到当前的最新值,之后再接收新的值。可以将其比作一个电灯,有开/关的状态,新回家的人(订阅者)可以立即看到当前是开还是关,然后会收到状态变化通知。

以下是 PassthroughSubject 的示例:


let messageRelay = PassthroughSubject<String, Never>()
let subscription1 = messageRelay.sink { message in
    print("订阅者1收到消息: \(message)")
}
messageRelay.send("你好")
messageRelay.send("世界!")
// 输出:
// 订阅者1收到消息: 你好
// 订阅者1收到消息: 世界!

以下是 CurrentValueSubject 的示例:


let textEditor = CurrentValueSubject<String, Never>("")
textEditor.send("初始文本")
let subscription2 = textEditor.sink { currentText in
    print("订阅者2收到文本: \(currentText)")
}
textEditor.send("更多文本")
// 输出:
// 订阅者2收到文本: 初始文本
// 订阅者2收到文本: 更多文本

Cancellable (可取消订阅)

在 Combine 中,我们通常使用 sinkassign 作为订阅者。sink 使用闭包来处理接收到的值,而 assign 则可以将接收到的值赋给对象的某个属性。

  • assign(to:on:): 将发布者的值赋给指定对象的某个属性。
  • assign(to:): 将发布者的值赋给另一个 Publisher。

sinkassign(to:on:) 方法都会返回一个 Cancellable 协议类型的对象。通过调用 Cancellable 对象的 cancel() 方法,可以手动停止接收数据,从而取消订阅。

示例代码:


// 假设 publishingSource 是一个 Publisher
let streamCancellable = publishingSource.sink { value in
    print("Sink 接收到值: \(value)")
}
// streamCancellable.cancel() // 取消订阅

// 假设 yourButton 是一个 UIButton 实例
let buttonCancellable = publishingSource
    .receive(on: RunLoop.main) // 确保在主线程更新 UI
    .assign(to: \.isEnabled, on: yourButton)
// buttonCancellable.cancel() // 取消订阅

// 将一个 publisher 的值赋给另一个 publisher
let assignmentCancellable = publishingSource.assign(to: &anotherPublisher)

AnyPublisher 和 AnyCancellable

AnyPublisherAnyCancellable 是类型擦除(Type Erasure)的包装器,用于隐藏具体的 Publisher 和 Cancellable 类型,方便统一处理。

  • AnyPublisher: 任何遵循 Publisher 协议的类型都可以通过 eraseToAnyPublisher() 方法转换为 AnyPublisher。这有助于在代码中统一处理不同类型的发布者,并隐藏其底层实现细节。
  • AnyCancellable: sinkassign 方法返回的 Cancellable 对象实际上是 AnyCancellable 的实例。AnyCancellable 必须被妥善存储(例如,保存在一个 Set 中),否则当它离开作用域时会被自动销毁,导致订阅失效。

使用 AnyPublisher 和存储 AnyCancellable 的示例:


let dataProvider: AnyPublisher<String, Never> = ["数据1", "数据2", "数据3"].publisher
    .eraseToAnyPublisher()

var activeSubscriptions = Set<AnyCancellable>()

dataProvider.sink { receivedData in
    print("收到数据: \(receivedData)")
}
.store(in: &activeSubscriptions) // 将 AnyCancellable 存储在 Set 中

// 输出:
// 收到数据: 数据1
// 收到数据: 数据2
// 收到数据: 数据3

@Published 属性包装器

@Published 是一个属性包装器,可以为类属性自动提供 Publisher 功能。当一个属性被 @Published 修饰时,它会创建一个对应的 Publisher,并且每次属性值发生变化时,这个 Publisher 都会发出新的值。

如果类的一个属性 value@Published 修饰,那么 $value 就会是一个 Published<Value>.Publisher 类型的 Publisher。

@Published 的使用示例:


class UserProfile {
    @Published var userName: String
    init(name: String) {
        self.userName = name
    }
}

let profile = UserProfile(name: "Alice")

// $profile.userName 是一个 Published<String>.Publisher
let profileSubscription = profile.$userName.sink { newName in
    print("用户名已更新为: \(newName)")
}

profile.userName = "Bob"
// 输出:
// 用户名已更新为: Bob
标签: Combineswift

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。