- 文档
【免费下载链接】swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.
SE-0463("Import Objective-C completion handler parameters as@Sendable")为 Swift 与 Objective-C 的并发互操作带来一项关键默认值改进:从 Swift 6.2 开始,导入的 Objective-C 方法若满足特定条件,其 completion handler(完成处理器)闭包参数将自动以@Sendable导入,从而让调用方在 actor 隔离边界上传入闭包时获得静态与动态的数据竞争安全保证。本文以该提案为主体,结合 SE-0297 的 async 互操作规则、SE-0423 的动态 actor 隔离机制以及 approachable-concurrency 愿景文档 中的迁移路径,深入讲解它的动机、设计规则、豁免情形、退出机制与兼容性考量。读完本文,你将理解为何缺失@Sendable会导致运行时崩溃、SE-0463 如何在导入层面自动修复这一问题,以及如何在 Clang 头文件中用@nonSendable显式退出该默认行为。
背景:@Sendable缺失如何演化成运行时崩溃
Swift 的数据竞争安全模型要求函数声明在签名中明确编码其并发不变量。@Sendable注解表示闭包参数在调用之前会被传递到一个隔离边界之外——也就是说,闭包可能在不同隔离域(如某个 actor 的执行器)中被调用,因此闭包内部不能捕获或访问非Sendable的可变共享状态。
当一个库的函数签名缺少@Sendable注解时,影响会传导到所有调用它的客户端:
- 调用方可能无意中引入数据竞争:调用方无法从签名得知闭包会被投递到其他隔离域执行,于是可能在闭包内捕获并修改 actor 隔离状态,造成数据竞争。
- 触发 SE-0423 的运行时断言:SE-0423 为从非严格并发上下文调用、且传入非
Sendable闭包参数的函数注入了动态 actor 隔离断言。当任何代码在 actor 隔离上下文中调用这类 API 时,缺失@Sendable注解可能导致运行时崩溃,而不是编译期诊断。
SE-0463 提案原文特别强调:这对于正在迁移到 Swift 6 语言模式的项目极其痛苦——迁移方(客户端)已经开启了严格并发检查,而被调用的 Objective-C 框架或尚未适配的库却没有补齐注解,结果错误地在运行时以崩溃形式暴露。approachable-concurrency 愿景文档的 "Mitigating runtime assertions due to isolation mismatches" 一节也把"自动将异步函数 completion handler 导入为@Sendable"列为缓解 SE-0423 运行时断言的明确路径之一(见 visions/approachable-concurrency.md)。
好消息是,有一大类带闭包参数的 API 即使缺少注解,也能被自动识别为@Sendable函数:带 completion handler 参数的 Objective-C 方法。对于这类方法,@Sendable几乎总是正确的默认值。
方案概览:导入时自动补上@Sendable
SE-0463 的核心提议非常直接:自动将来自 Objective-C 方法的 completion handler 参数导入为@Sendable函数,无需修改任何 Objective-C 头文件。
这本质上是 Clang 导入规则(Clang importer)的一项改动:编译器在把 Objective-C 声明翻译为 Swift 时,对识别出的 completion handler 参数自动附加@Sendable。开发者无需逐个头文件审计并手动标注,这正是社区在 Clang sendability 审计讨论 中一直寻求的自动化方案。
详细设计:触发条件与导入效果
触发条件:async 变体 + nonisolated
并非所有带 completion handler 的 Objective-C 方法都会被导入为@Sendable。SE-0463 设定的触发条件是:
- 导入的方法存在 async 变体——即该方法满足 SE-0297 中异步 completion-handler 方法的识别规则(见下节回顾);
- 该方法(隐式或显式地)是
nonisolated。
满足这两个条件时,原始方法将带着@Sendable注解的 completion handler 参数被导入。
SE-0297 的 async 变体识别规则回顾
要理解"存在 async 变体"的判断标准,需要回顾 SE-0297 的 heuristics。一个 Objective-C 方法被视为"潜在的异步 completion-handler 方法"需要满足:
- 方法本身返回
void(所有结果通过 block 交付); - completion handler 是返回
void的 block,且在所有执行路径上恰好被调用一次; - 若方法可能报错,block 中应有一个非
_Nonnull的NSError *参数。
当方法未显式使用swift_async属性标注时,编译器通过以下命名规则推断 completion handler:
- 若方法只有单一参数,且第一个 selector 片段以
WithCompletion、WithCompletionHandler、WithCompletionBlock、WithReplyTo、WithReply之一结尾,则该参数即 completion handler,匹配短语会从导入的 Swift 函数名中移除; - 若方法有多个参数,且最后一个参数的 selector 片段或参数名为
completion、withCompletion、completionHandler、withCompletionHandler、completionBlock、withCompletionBlock、replyTo、withReplyTo、reply或replyTo,则最后一个参数是 completion handler; - 若方法有多个参数,且最后一个参数以上述后缀结尾,后缀前的文本会追加到 Swift 函数基名中。
此外,SE-0297 还提供__attribute__((swift_async(...)))等 Clang 属性来显式控制转换(none关闭转换、指定索引参数为 handler、控制throws与_Nullable_result语义等)。SE-0463 正是在这些规则识别出的 completion handler 参数上追加@Sendable。
导入示例
提案原文给出了如下 Objective-C 方法签名:
- (void)performOperation:(NSString * _Nonnull)operation completionHandler:(void (^ _Nullable)(NSString * _Nullable, NSError * _Nullable))completionHandler;Swift 将把它导入为带@Sendable的版本:
@preconcurrency func perform( operation: String, completionHandler: @Sendable @escaping ((String?, Error?) -> Void)? )注意两个细节:
- 参数类型仍是可选闭包(
(…)?),保留了 Objective-C 侧_Nullable的语义; - 函数被标注为
@preconcurrency——这正是下面要展开的兼容性关键。
从 actor 调用时的行为变化
当从 Swift actor 调用perform方法时,此前"允许非Sendable闭包被隔离到其形成上下文"的推断规则将不再适用。具体表现:
- 闭包会被推断为
nonisolated; - 若闭包内访问了 actor 区域中的可变状态,编译器将产生警告。
需要特别说明的是,由于所有从 C/C++/Objective-C 导入的 API 都自动带有@preconcurrency语义(SE-0337 明确规定"Objective-C declarations are always imported as though they were annotated with@preconcurrency",见 proposals/0337-support-incremental-migration-to-concurrency-checking.md),数据竞争安全违规即使在 Swift 6 语言模式下也只会产生警告,不会升级为错误。这保证了迁移期间代码仍然可以编译运行。
例外:全局 actor 隔离函数的 completion handler 不导入为@Sendable
SE-0463 为隔离到全局 actor(global actor)的函数设置了一个重要豁免:这类函数的 completion handler不会被导入为@Sendable。
理由非常务实:"主 actor 隔离的函数其 completion handler 总是在主 actor 上被调用"是 Objective-C 中极其常见的模式。如果一律导入为@Sendable,当头文件中恰好缺少 completion handler 参数上的主 actor 标注时,就会产生大量误报警告。该豁免不会引入任何新的动态断言,因此不会带来新的运行时崩溃风险。
这是提案在 pitch 讨论后修订加入的内容(见提案末尾 Revisions 一节),是权衡"减少误报"与"保持安全"后的折中。
退出机制:用@nonSendable标注不需要@Sendable的 handler
如果某个 completion handler 在调用前不会跨越隔离边界(例如保证在相同隔离域内同步调用),可以在 Objective-C 头文件中用@nonSendable属性显式退出 SE-0463 的默认导入行为:
__attribute__((swift_attr("@nonSendable")))SE-0297 中定义的__attribute__((swift_attr("...")))是一个通用 Clang 属性,允许直接在 Objective-C 声明上书写任意 Swift 属性(例如全局 actor 标注@MainActor)。SE-0463 复用这一机制,将@nonSendable作为 Clang 头文件注解的专用形式。
需要强调的是:@nonSendable只用于 Clang 头文件注解,不用于 Swift 代码。Swift 侧抑制Sendable的机制是~Sendable泛型参数标记(见 SE-0518),二者面向的场景不同,不要混淆。
兼容性分析
源兼容性(Source compatibility)
SE-0463 的兼容性设计高度依赖@preconcurrency的降级机制(源自 SE-0337):
- 在 Swift 6 之前的语言模式下使用最小并发检查(minimal concurrency checking)时,本改动没有任何效果;
- 使用完整并发检查(complete concurrency checking)时,即使在 Swift 6 语言模式下,也只会引入警告;
- 根本原因:从 C/C++/Objective-C 导入的声明隐式带有
@preconcurrency,它把所有数据竞争安全违规都降级为警告。
因此,存量代码不会因为升级语言模式而突然编译失败,这为大型项目渐进迁移到 Swift 6 提供了缓冲。
ABI 兼容性
提案明确指出:对现有 ABI 没有任何影响。@Sendable是纯编译期注解,不改变调用约定、不增加运行时元数据,因此导入规则的改动完全不影响二进制兼容。
备选方案:为什么用@Sendable而不是sending
SE-0463 在备选方案一节详细讨论了"将 completion handler 导入为sending参数"的替代路线,并解释了为何最终选择@Sendable:
- 实现历史与验证:实验性的
SendableCompletionHandlers实现自 2021 年起就存在,经过了大量源兼容性测试;@Sendable也已被 Objective-C 框架显式采用多年,编译器实现对@Sendable不匹配的边界情况已经过充分打磨。 sending成熟度不足:sending仍是相对较新的参数属性,生态采用度远不如@Sendable,并且与@preconcurrency结合时不支持在 Swift 6 语言模式下降级诊断——这直接破坏了上文所述的兼容性缓冲。- SE-0423 运行时断言的紧迫性:动态 actor 隔离断言带来的崩溃痛苦足够严重,值得先用
@Sendable保守地解决问题。
提案同时预警:将来若改为sending,会引入源兼容性问题——协议要求以sendingcompletion handler 导入后,用@Sendablecompletion handler 实现该要求将变得非法;类方法重写也存在同样问题。因此,如果开发者想利用区域隔离(region isolation)的更强保证,推荐路径是使用async/await现代化代码(即调用 SE-0297 导入的 async 变体),而不是依赖 completion handler 签名。
与相关提案的协作关系
| 提案 | 角色 |
|---|---|
| SE-0297 | 定义 completion handler 的识别规则与 async 变体导入;SE-0463 的触发条件"存在 async 变体"以此为基础 |
| SE-0423 | 对非Sendable闭包注入运行时断言;SE-0463 的动机直接来自其崩溃风险,且提供-disable-dynamic-actor-isolation作为应急开关 |
| SE-0337 | 提供@preconcurrency降级机制,使 SE-0463 的所有诊断保持为警告 |
| approachable-concurrency 愿景 | 将自动@Sendablecompletion handler 列为缓解运行时断言、提升数据竞争安全可及性的官方路线 |
| SE-0518 | 提供 Swift 侧~Sendable泛型标记,与 Clang 侧的@nonSendable注解形成互补 |
从实现角度看,SE-0463 是 Clang importer 层面的规则变更:编译器在按 SE-0297 规则翻译 Objective-C 方法时,对满足"存在 async 变体且 nonisolated"条件的方法的 completion handler 参数自动附加@Sendable。它不要求修改任何头文件,也不要求开发者书写新注解,属于"默认即安全"的导入改进。
实践要点总结
- 升级到 Swift 6.2 后,符合条件的 Objective-C completion handler 方法会自动以
@Sendable导入,你无需改动任何代码即可获得更强的数据竞争检查; - 从 actor 调用这类 API 时,闭包被推断为
nonisolated,访问 actor 隔离状态会得到警告(而非错误),请据此修正闭包内对共享可变状态的访问; - 若某个 handler 确实不跨越隔离边界,在 Objective-C 头文件中用
__attribute__((swift_attr("@nonSendable")))标注即可退出默认行为; - 全局 actor 隔离的 Objective-C 方法不受此规则影响,其 completion handler 仍按原有方式导入,不会产生新的误报或动态断言;
- 追求更强区域隔离保证时,优先将代码现代化为
async/await形态(SE-0297 导入的 async 变体),而非依赖 completion handler 签名层面的sending。
- 文档
【免费下载链接】swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.
相关推荐
Midscene.js 接入 Playwright:3 步搞定视觉化端到端测试
Midscene.js 接入 Playwright:3 步搞定视觉化端到端测试 副标题:别再追着选择器改了——用自然语言描述操作,让多模态模型完成定位与点击。
文档Swift 3 桥接机制核心提案 SE-0116:将 Objective-C `id` 导入为 Swift `Any` 类型
Swift 3 桥接机制核心提案 SE 0116:将 Objective C id 导入为 Swift Any 类型 SE 0116("Import Objec
文档10个Tullio.jl实用案例:从矩阵乘法到高级张量收缩
10个Tullio.jl实用案例:从矩阵乘法到高级张量收缩 Tullio.jl 是 Julia 生态中最灵活的张量收缩(Tensor Contraction)与
文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考