对,这就把事情彻底坐实了 ✅
你现在的工程处于这种状态:
MutableReferenceExample(以及它的存储属性 counter)被默认推断为 @MainActor 隔离
所以你在 Task.detached(不在 MainActor)里访问它,就会报:
Main actor-isolated property ‘counter’ cannot be accessed from outside of the actor
这也解释了你之前所有“看起来 Sendable 通过/不通过很奇怪”的现象:
✅ 传递可以
Task.detached { takesSendable(instance) }因为只是把一个“MainActor 管辖的引用”传过去(编译器允许你持有引用本身),但…
❌ 访问不行
Task.detached { _ = instance.counter }因为真正访问隔离状态必须回到 MainActor。
你现在得到的核心结论
1) 你之前看到 class “通过 Sendable”并不是因为 class 自动线程安全
而是因为:
MainActor 隔离替代了 Sendable 检查
线程安全由 “单线程 actor(MainActor)串行化访问” 来保证。
这是一条非常重要的 Swift 6 设计路线:
actor isolation 比 Sendable 更高优先级、更像运行时的“访问权限系统”。
那你到底该怎么设计你的协议/Presenter?
结合你 AIChats 的场景,我给你一个非常实用的分界线:
✅ UI 层(View / Presenter / AppState)——就让它 MainActor
因为它本来就不该脱离主线程。
@MainActor final class Presenter { ... }
或者依赖 target 的 Default Actor Isolation(你现在就是这样)
这时:
不需要强行 Sendable
也不应该 Task.detached 去摸它的属性
如果要在后台算东西:
Task.detached {
let result = heavyCompute()
await MainActor.run {
presenter.apply(result)
}
}✅ Service 层(网络 / Firebase / 存储)——不要让它被 MainActor “吸住”
你现在 FirebaseAuthService 是 struct,很好。
但如果你写 protocol service:
protocol AuthService { ... }我建议:
Service 不标 MainActor
如果需要跨线程安全,用 actor 或显式 Sendable
例如:
protocol AuthService: Sendable {
func signIn() async throws -> User
}
struct FirebaseAuthService: AuthService { ... } // struct 很容易 Sendable你问的最初问题:“protocol 现在是不是 Sendable 了?”
你已经自己验证出了正确答案:
✅ protocol 关键字没有变
✅ protocol P: Sendable 只是约束
✅ 你现在看到的“很多东西看起来 Sendable”主要是因为:
Default Actor Isolation = MainActor
导致大量类型/成员自动 MainActor 隔离
最后给你一句“工程级建议”(避免以后继续迷惑)
如果你想研究 Sendable 本身
请在一个独立 target / Swift Package 里测试,并确保:
Default Actor Isolation 不是 MainActor
不 import SwiftUI
用 Task.detached / @Sendable closure 做触发器
如果你想做 SwiftUI App(AIChats)
那你现在这个默认 MainActor 策略其实挺合理,关键是:
UI 层别 detached
后台任务算完再回 MainActor 更新状态
如果你愿意,我可以根据你 AIChats 的模块划分,给你一份“并发分层规范”:
哪些文件强制 @MainActor
哪些文件强制 nonisolated
哪些地方必须 actor
Presenter / Service / Environment 注入的最佳姿势(不产生 Sendable 警告、也不把业务写进 View)