惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
博客园 - 【当耐特】
S
SegmentFault 最新的问题
阮一峰的网络日志
阮一峰的网络日志
大猫的无限游戏
大猫的无限游戏
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Cloudflare Blog
Apple Machine Learning Research
Apple Machine Learning Research
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Fortinet All Blogs
Y
Y Combinator Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
J
Java Code Geeks
Engineering at Meta
Engineering at Meta
MyScale Blog
MyScale Blog
B
Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理

肘子的 Swift 记事本 | Fatbobman's Blog

从 pbxproj 到 xcproj:Xcode 工程配置迎来 JSON 格式 iPhone Duo 带来的机遇与挑战 -- 肘子的 Swift 周报 #153 当 Mac mini 的价格不再 mini -- 肘子的 Swift 周报 #152 SwiftData:优化从建模开始 举手之劳 -- 肘子的 Swift 周报 #151 从使用 AI 到委托 AI(下):我想要的可委托性 AI 想放弃了,人没有 -- 肘子的 Swift 周报 #150 从使用 AI 到委托 AI:我的一些思考 越来越慢的 App Review,拦住了谁? -- 肘子的 Swift 周报 #149 ContentBuilder 解析:SwiftUI 类型检查性能提升的秘密 Apple Intelligence 已通过中国政府审核,即将在中国提供服务 -- 肘子的 Swift 周报 #148 在 SwiftUI Text 中控制孤行:挖掘未公开的 avoidsOrphans 热茶还是冰咖啡 -- 肘子的 Swift 周报 #147 Liquid Glass:UIKit 适配踩坑实录 不是模型变慢了,是任务变大了 -- 肘子的 Swift 周报 #146 当灵感跑在了结果前面 -- 肘子的 Swift 周报 #145 当每一次写入都有了新价格 -- 肘子的 Swift 周报 #144 当 Linux 成为“空气”:容器、Agent 与不再重要的“桌面之争” -- 肘子的 Swift 周报 #143 两个 SwiftUI 动画 Bug 排查小记 SPI 加入 Apple,Swift 迈向自举 -- 肘子的 Swift 周报 #142 Swift 还让你 Excited 吗? -- 肘子的 Swift 周报 #141 从 Size Class 到可用空间,horizontalSizeClass 还可靠吗? WWDC 26:AI 帮你看完了,然后呢? -- 肘子的 Swift 周报 #140 WWDC 2026 初印象:符合预期,但更务实 -- 肘子的 Swift 周报 #139 Core Data + Observation:从属性级响应到心智解放 稳定 > 新功能 -- 肘子的 Swift 周报 #138 用自定义 Layout 化解 SwiftUI List 的行高与间距跳变 从社区路标到生态基石:Dave Verwer 的新篇章 -- 肘子的 Swift 周报 #137 消失的 WWDC 愿望单 -- 肘子的 Swift 周报 #136 CocoaPods 正在退场,SwiftPM 才刚到第二章 -- 肘子的 Swift 周报 #135
isolated(any) 与 #isolation:让 Swift 闭包自动继承隔离域
东坡肘子 · 2026-01-21 · via 肘子的 Swift 记事本 | Fatbobman's Blog

Swift 6 为并发引入了许多新功能与关键字。虽然其中不少内容在日常开发中可能鲜少用到,但一旦遭遇特定场景,若对这些新概念缺乏了解,即便有 AI 辅助也可能陷入僵局。本文将通过一个在开发测试中遇到的实际并发问题,来介绍如何利用 @isolated(any) 以及 #isolation 宏,实现函数的隔离域继承,从而让编译器自动推断闭包的运行环境。

编译器忽视了我的 Default Actor?

在 Swift 6.2 推出 Default Actor Isolation(默认 Actor 隔离)功能后,我一直在积极探索其适用场景。由于我习惯通过 SPM 将不同的功能模块分配到独立的 Target 中,而 Default Actor Isolation 又是以 Target 为单位进行设定的,因此在构建视图功能集或状态/数据流功能集时,我通常会添加如下设置:

swiftSettings: [
    .swiftLanguageMode(.v6),
    .defaultIsolation(MainActor.self),
    .enableExperimentalFeature("NonisolatedNonsendingByDefault"),
]

这确实显著减轻了编码与测试时的心智负担。一个立竿见影的效果是,在这些 Target 中,我已经很久不需要在闭包里使用显式的 @MainActor 标注了:

// 以前常需这样写,现在通常可以省略
{ @MainActor in
  ....
}

不过,最近在使用我自己编写的一个简易依赖注入库进行测试时,却出现了预期外的情况。我被迫必须加上 @MainActor in,否则代码无法通过编译:

@Test
func switchID() async {
    let noteID = UUID()
    await withDependencies {
        $0.appSettings = AppSettingsTestHelpers.makeMockSettings()
    } operation: {
        @Dependency(\.appSettings) var settings
        // 报错:Main actor-isolated property 'noteID' can not be referenced from a Sendable closure
        settings.noteID = noteID 
        ...
    }
}

编译器报错提示:Main actor-isolated property ‘noteID’ can not be referenced from a Sendable closure”(无法从 Sendable 闭包中引用 MainActor 隔离属性 ‘noteID’)。

尽管可以通过手动添加 @MainActor in 解决问题,但这引发了我的困惑:在 Target 的 Default Actor 既然已经设置为 MainActor 的前提下,编译器为何无法自动推断出 operation 闭包应该运行在 MainActor 上?

分析问题

上述代码中使用的 withDependencies 是我模仿 swift-dependencies API 风格编写的简化版:TinyDependency。它仅有一百多行代码,无第三方依赖,完全满足我的个人需求。

其声明如下:

public func withDependencies<R>(
    _ updateValuesForOperation: (inout DependencyValues) -> Void,
    operation: () async throws -> R
) async rethrows -> R {
    var dependencies = DependencyValues.current.copy()
    updateValuesForOperation(&dependencies)
    return try await DependencyValues.$_current.withValue(dependencies) {
        try await operation()
    }
}

根据我对 Default Actor Isolation 的理解,既然我在该 Target 中声明的协议、类型和方法都默认归属于 @MainActor,那么在测试方法中,withDependencies 接收的 operation 异步闭包,理应被 Swift 编译器推断为在 MainActor 上运行。

然而现实是,编译器并未按此预期进行推断。它似乎认为这个闭包可能会跨 Actor 执行,因此阻止了对 MainActor 隔离属性的访问。虽然我可以用 @MainActor in 进行运行时强制限定,但有没有办法能让编译器在编译时就自动完成推断呢?

让闭包继承隔离域

为了解决这个问题,我们可以利用 Swift 6 引入的两个新特性。

首先是 SE-0431 提出的 @isolated(any)。它的主要目的是解决函数作为值传递时,编译器缺乏足够信息来准确判定隔离域的问题。

当使用 @isolated(any) 标注一个函数类型时,该函数类型将携带其调用者所在的隔离域信息。虽然我们在代码中通常不会直接读取这个信息,但编译器会利用它来推断运行时的隔离环境。

withDependencies 修改如下,之前的编译错误便消失了:

public func withDependencies<R>(
    _ updateValuesForOperation: (inout DependencyValues) -> Void,
    operation: @isolated(any) () async throws -> R // 增加了 @isolated(any)
) async rethrows -> R {
    var dependencies = DependencyValues.current.copy()
    updateValuesForOperation(&dependencies)
    return try await DependencyValues.$_current.withValue(dependencies) {
        try await operation()
    }
}

通过添加 @isolated(any),编译器在处理 operation 闭包时,会自动感知并继承调用者的隔离域。由于测试 Target 的默认隔离域是 MainActor,因此编译器推断该闭包也运行在 MainActor 上,从而通过了编译时的安全检查,无需再手动书写 @MainActor

不过,@isolated(any) 更多是针对函数类型的修饰。如果我们要将其作为一个通用的库函数,可能不希望局限于这一种推断方式。此时,我们可以采用 SE-0420 提出的 隔离继承(Inheritance of actor isolation) 方案:

public func withDependencies<R>(
    isolation: isolated (any Actor)? = #isolation, // SE-0420: 隔离继承
    _ updateValuesForOperation: (inout DependencyValues) -> Void,
    operation: () async throws -> R
) async rethrows -> R {
    var dependencies = DependencyValues.current.copy()
    updateValuesForOperation(&dependencies)
    return try await DependencyValues.$_current.withValue(dependencies) {
        try await operation()
    }
}

通过为 withDependencies 增加一个 isolation 参数,并配合 #isolation 宏,我们能够提前明确 operation 的隔离上下文。

这里的 isolation 参数提供了三种可能性:

  • nil:函数是动态非隔离的,这与未添加该参数时的默认行为一致。
  • Global Actor:函数动态隔离至指定的全局 Actor。例如,传入 MainActor.shared 可以代替闭包中的 @MainActor in,将运行时检查提前到编译时。
  • 调用者的隔离域:如果从某个 Actor 实例中调用 withDependencies,它将继承该 Actor 的隔离域。

为了实现“默认继承调用者隔离域”的效果,我们使用了 #isolation 宏作为默认参数:

/// 获取当前代码所在的 Actor 隔离引用,若代码非隔离则返回 nil。
@freestanding(expression) public macro isolation<T>() -> T = Builtin.IsolationMacro

它可以自动捕获当前的隔离信息并传递给 isolation 参数。经过上述调整,我的测试代码不仅能够正常编译运行,而且无需手动追加任何额外的隔离域标注,代码显得更加简洁干净。

冷门但有用

Swift 6 新增的众多并发关键字确实容易给开发者带来困扰。阅读 Proposal 或技术文章时,很多关键字常让人感到“莫名其妙”甚至“画蛇添足”。

isolated(any) 为例,我最早是通过 Matt Massicotte 的文章了解到的,甚至还在周报中推荐过。但即便如此,如果不是这次在测试中碰壁,再加上我已经习惯了不写 @MainActor in,我可能很难想起去使用它。

然而,真正上手后就会发现,这些功能确实体现了 Swift 6 极致追求编译时安全的精神。它们或许冷门,但在特定场景下,确实非常有用!