
当前时间:2077年,一个阴雨连绵的周二
地点:Neo-Cupertino,第 42 区,“无限循环” 咖啡馆
人物:
-
Jet:资深架构师,义眼闪烁着蓝光的代码老兵,热衷于复古的 Swift 语法。
-
Nova:刚入行的初级工程师,满脑子是最新的神经链接框架,但经常被编译器暴揍。
-
反派:“The Race Condition” (竞态幽灵),一个游荡在系统内存缝隙中的古老 AI 病毒,专门吞噬不安全的变量。
窗外的霓虹灯光透过雨幕,在 Jet 的合成皮风衣上投下斑驳的阴影。他抿了一口手中的高浓度咖啡因液,看着面前焦头烂额的 Nova。
“我的编译器又在尖叫了,” Nova 把全息屏幕推向 Jet,上面红色的错误提示像鲜血一样流淌,“我只是想在一个 Actor 里用个简单的类,结果 Swift 的并发检查像个疯狗一样咬着我不放。我是不是该把所有东西都加上 @MainActor 算了?”

Jet 叹了口气,那是见惯了无数次堆栈溢出后的沧桑。“滥用主线程隔离?那是饮鸩止渴。Nova,你被恐惧蒙蔽了双眼。来,看看这份 2025 年的加密文档。那时候我们管这个叫——Non-Sendable First Design。”
在本篇博文中,您将学到如下内容:
- 🦾 序章:尴尬却必要的兴奋
- 🧠 核心概念重载 (Quick Refresher)
- 🔒 困兽之斗?不,是绝对领域 (Getting Stuck)
- ⚖️ 权衡:光与影 (The Pros and Cons)
- 💎 优势 #1:极致的简单 (Simplicity)
- 🌐 优势 #2:通用性 (Generality)
- 🕳️ 弱点 #1:启动任务的陷阱 (Starting Tasks)
- 💊 并不完美的解药
- 🛑 启动任务本身就是一种充满危险的诱惑
- 🕸️ 弱点 #2:诡异的属性 (Weird Properties)
- 🎬 终章:回归本源 (First for a Reason)
Jet 手指轻挥,将文档投影在两人中间,开始了他的解构。

🦾 序章:尴尬却必要的兴奋
Jet 指着文档的开头说道:“这作者是个老实人。他承认自己对 Non-Sendable Types (非跨域传输类型) 的痴迷程度简直到了走火入魔的地步,甚至有点尴尬。”
“这就好比给自己的战术命名一样,听起来有点自以为是。但他把这种设计理念称为 ‘Non-Sendable First Design’。虽然名字听起来像是什么二流黑客的代号,但其核心思想在当年可是振聋发聩。”

“在那个年代,因为语言特性的限制,大家对 Non-Sendable 类型避之唯恐不及。它们虽然有用,但用起来就像是在满是碎玻璃的地上跳舞——人体工程学极差。直到有一天,NonisolatedNonsendingByDefault(即 Swift 6 中的‘可接近并发’特性)横空出世。这一切都变了!Non-Sendable 类型突然变得顺滑无比,仿佛这才是它们原本的宿命。”
🧠 核心概念重载 (Quick Refresher)
“在深入之前,我们要先进行一次思维格式化。” Jet 的义眼转动,调出了基础理论图谱。
Swift 解决 Data Race (数据竞争) 的手段非常硬核:它要求在编译时就确立对非线程安全数据的保护。这种抽象被称为 Isolation (隔离)。
-
实现层:可能是锁 (Lock)、队列 (Queue),或者是专用的线程。
-
运行时:由 Actor 负责具体的保护机制。
“编译器其实是个‘脸盲’,” Jet 解释道,“它不知道某个 Actor 到底是怎么运作的(比如 MainActor 其实是把任务倒进主线程这个大漏斗里),它只知道一点:Actor 能护犊子。”

Swift 将数据世界一分为二:
-
Sendable (可传输类型):天生的战士,线程安全,可以在并发的枪林弹雨中随意穿梭,无需保护。
-
Non-Sendable (非可传输类型):共享的可变状态。它们是我们程序中最有趣、最核心的部分,比如你的用户数据、缓存状态。但它们也是脆弱的,就像没有穿护甲的平民,必须被保护。
“很多人误以为 Actor 只是用来‘后台运行’的工具,这完全是买椟还珠。Actor 的真正使命,是充当 Non-Sendable 数据的保镖,防止它们被竞态幽灵吞噬。”
🔒 困兽之斗?不,是绝对领域 (Getting Stuck)
“听着,Nova。世界上最好的安保系统,如果不允许任何人进出,那也没用。但在 Swift Concurrency 的法则里,有一个非常有趣的特性:”
如果一个 Actor 拥有(通常是创建)了一个 Non-Sendable 类型,这个类型就被‘困’住了。
“想象一下,” Jet 描绘道,“一个由类和协议组成的庞大网络,彼此交织,协同工作。它们可以在 Actor 的围墙内为所欲为。但编译器这个冷酷的守门人,绝对禁止你不安全地将它们扔到墙外。”

“这听起来像是限制,但这正是 Non-Sendable 类型的强大之处——画地为牢,反而成就了绝对的安全。”
⚖️ 权衡:光与影 (The Pros and Cons)
Jet 调出了对比数据面板。“作者曾纠结于如何展示这些,最后他决定返璞归真,先给你看甜头,再告诉你陷阱。注意,这里默认的语境是 nonisolated (非隔离) 的。”
💎 优势 #1:极致的简单 (Simplicity)
“看这段代码,Nova。它简单得像是一首儿歌。”
class Counter {
var state = 0
func reset() {
self.state = 0
}
}
“这就是一个普通的类,甚至还有可变状态。再看这一步:”
class Counter {
// ...
// 这里加上了 async
func toggle() async {
self.state += 1
}
}
extension Counter: Equatable {
static func == (lhs: Counter, rhs: Counter) -> Bool {
lhs.state == rhs.state
}
}
Jet 敲着桌子强调:“这里有两个关键点!”
-
Async 方法:在旧时代,
nonisolated + async 意味着代码会跑去后台线程,但这对于 Non-Sendable 类型来说是个悖论(它不能离开 Actor 的保护)。这曾经是个第22条军规式的死锁。但现在,有了 NonisolatedNonsendingByDefault,这个问题迎刃而解。
-
协议一致性 (Protocol Conformance):看那个
Equatable。这对于普通类来说易如反掌。但如果是 MainActor 隔离的类型?那简直是噩梦,你得处理各种隔离上下文的匹配问题。
“Non-Sendable 类型拥有隔离类型所不具备的纯粹和简单。”

🌐 优势 #2:通用性 (Generality)
“这需要一点悟性,” Jet 眯起眼睛,“这关乎同步访问 (Synchronous Access)。”
“如果你给类型加上了 @MainActor,那只有主线程的朋友才能同步访问它。这就像是个 VIP 俱乐部。但 Non-Sendable 类型是通用的雇佣兵。”
actor ActorClient {
// ActorClient 拥有这个 counter
private let counter = Counter()
func accessSynchronously() {
// ✅ 这是完全可能的!
// 因为 counter 是 Non-Sendable,它被"困"在了 ActorClient 的隔离域内,
// 所以 ActorClient 可以像操作自家后院一样同步操作它。
counter.reset()
}
}
“它的接口保持不变,不管是谁‘拥有’它。这就是海纳百川的通用性。”

🕳️ 弱点 #1:启动任务的陷阱 (Starting Tasks)
此时,全息投影变成了警告的红色。
“这就是那个让我开始反思的地方,” Jet 指着下面的代码,“也是新手最容易踩的雷区。”
class Counter {
// ...
func printStateEventually() {
Task {
// ❌ 错误: 将闭包作为 'sending' 参数传递...
// 编译器会阻止你,因为你试图在新的 Task 里捕获非 Sendable 的 self
print(self.state)
}
}
}
“为什么不编译?把它翻译成古老的 GCD 你就懂了:”
// ⚠️ 警告: 在 @Sendable 闭包中捕获了 non-Sendable 类型 'Counter'
DispatchQueue.global().async {
print(self.state)
}
“这就像你在没有任何保护措施的情况下,试图把一个易碎的花瓶扔到正在高速运转的传送带上。在一个 nonisolated 的函数里启动 Task,意味着上下文是不确定的。没有线程安全的保证,编译器绝不会放行。”

💊 并不完美的解药
“为了解决这个问题,我们要么回到原来的队列(像 GCD 那样),要么把 isolation 参数传进去。”
class Counter {
// 显式传递隔离上下文,这代码丑得像被辐射过的变异体
func printStateEventually(isolation: isolated any Actor) {
Task {
_ = isolation // 这里的魔法是为了继承隔离域
print(self.state)
}
}
}
“这方案有毒,” Jet 摇摇头,“第一,它有传染性,调用者必须提供 Actor。第二,这语法太啰嗦。最重要的是……”

🛑 启动任务本身就是一种充满危险的诱惑
“Jet 突然严肃起来:“有人曾告诉我,不应该在这种类型内部创建非结构化的 Task。以前我觉得那是废话,现在我觉得那是金玉良言。”
“在类型内部悄悄启动一个外部无法等待 (await) 的任务,这是埋雷。这让代码变得难以观测、难以测试。这其实是一种语法盐 (Syntactic Salt)——语言故意让你难受,是为了告诉你:别这么干!”
“正确的做法是使用 async 方法,保持在结构化并发的世界里。如果非要开新任务,让调用者去开。”

🕸️ 弱点 #2:诡异的属性 (Weird Properties)
“还有一些边缘情况,” Jet 快速带过,“比如 lazy var、属性包装器 (Property Wrappers) 或者宏 (Macros)。如果在这些东西里混合复杂的并发要求,你会发现自己进退维谷。”
class Counter {
// 这种延迟加载在并发环境下可能极其复杂
lazy var internal = {
SpecialCounter(isolatedParam: ???)
}()
}
“这虽然罕见,但一旦遇到,就像是撞上了隐形墙。前车之鉴,不可不防。”

🎬 终章:回归本源 (First for a Reason)
雨渐渐停了,Neo-Cupertino 的黎明即将来临。Jet 关闭了全息投影,看着若有所思的 Nova。
“‘Non-Sendable First’,不是让你永远只用这一招,而是把它作为起点。”

“在 Swift 5.5 之前,所有东西本质上都是 nonisolated 的。这才是‘常态’。只要不涉及跨线程,它们就是最轻量、最高效的选择。”
“当然,当你遇到真正的并发需求,当你发现无法在 Actor 之间安全传递数据时,再考虑加上 Sendable 或 Actor 约束。但在那之前……” Jet 站起身,整理了一下衣领。
“不要为了并不存在的并发问题,去背负隔离带来的沉重枷锁。 利用 Region Isolation (区域隔离) 的特性,让编译器帮你推导安全性。这种感觉就像是作弊,但却是合法的。”

Nova 看着屏幕上终于变绿的编译通过提示,眼中闪过一丝光芒。“所以,大道至简?”
“没错,” Jet 转身走进晨雾中,留下最后一句话,“如果觉得复杂,说明你走错了路。Non-Sendable 的世界,简单得让人上瘾。”
