
引子
话说康熙年间,iOS 江湖正值 "版本迭代" 的乱世 ------Swift 6 携 "并发安全" 的大旗横空出世,往日里被开发者奉为 "偷懒神器" 的单例招式,一夜之间成了 "违规禁招"。
天地会总舵主陈近南刚收到线报,不少分舵兄弟因沿用旧单例写法,导致 App 频繁闪退,连给康熙爷呈递的 "奏章 App" 都差点出了纰漏。

深知此事关乎帮派声誉,陈近南连夜将韦小宝召至总舵,递上一本封皮烫金的《Swift 6 单例秘籍》,沉声道:"小宝,你在皇宫里见多识广,这单例就像你藏的'四十二章经',既是刚需又藏着风险。如今 Swift 6 立下新规矩,若再用老办法,轻则丢了差事,重则连累天地会!"
在本篇帮规中,各位少侠将学到如下内容:
- 引子
- 📜 第一章:单例的 "江湖往事"------ 为何人人又爱又恨?
- ⚖️ 第二章:Swift 6 的新铁规 ------ 为何单例突然 "违法"?
韦爵爷虽不懂高深代码武功,却凭着 "察言观色、见招拆招" 的本事,决定先把这单例的 "前世今生" 和新规矩摸个通透。
📜 第一章:单例的 "江湖往事"------ 为何人人又爱又恨?
在 Swift 旧江湖里,单例是绝对的 "流量明星"。
比如天地会要验证兄弟身份,得有个 "身份验证总舵"(AuthProvider);骁骑营要统计全军战力,得有个 "战力账本"(GamePiece);甚至韦小宝的 "丽春院记账 App",都得有个 "全局账本实例"------ 总不能让每个模块都单独建个 "总舵" 或 "账本",那岂不是乱了套?

老江湖写单例,向来简单粗暴,如同韦爵爷掏匕首般干脆:
swift
class AuthProvider {
// 全局共享的"总舵印信",全江湖就这一个
static let shared = AuthProvider()
// 存储兄弟令牌,验证身份时要用
private var memberTokens: [String: Bool] = [:]
// 验证令牌是否有效------这招是总舵核心功夫
func verifyToken(_ token: String) -> Bool {
return memberTokens[token] ?? false
}
// 私有初始化,防止外人仿造"印信"------这点很关键!
private init() {}
}
用的时候更方便,各分舵兄弟随用随调:
swift
// 杭州分舵验证令牌
let isValid = AuthProvider.shared.verifyToken("天地会-杭州-001")
可这单例的 "坏名声",比韦爵爷的 "流氓行径" 传得还快。最大的问题就是 "全局可变状态"------ 就像那本 "战力账本",若是两个小兵同时修改:
swift
// 小兵甲在主线程给战力加10
GamePiece.shared.power += 10
// 小兵乙在后台线程给战力减5
DispatchQueue.global().async {
GamePiece.shared.power -= 5
}
结果可能是战力值算错(数据竞态),严重时直接让 "战力统计 App" 闪退 ------ 这就像韦小宝算错丽春院账目,轻则被老妈子骂,重则赔光家底。

不过江湖前辈们早有对策:改用 "显式依赖注入"(DI),如同韦小宝办事前先亮身份、交凭证,明明白白不藏私。比如给杭州分舵 "送" 一个 AuthProvider 实例,而非让他们直接抢 "总舵印信":
swift
// 给杭州分舵传一个 AuthProvider 实例
class HangzhouBranch {
private let auth: AuthProvider
// 初始化时明确传入"身份验证工具",不偷偷用全局单例
init(auth: AuthProvider) {
self.auth = auth
}
// 用传入的实例验证令牌,安全可控
func checkMemberToken(_ token: String) -> Bool {
return auth.verifyToken(token)
}
}
// 使用时主动创建实例并传入
let auth = AuthProvider()
let hangzhouBranch = HangzhouBranch(auth: auth)
此法虽麻烦,却能避免 "全局状态混乱",代码的 "可测试性" 也大大提升 ------ 就像韦小宝做账时留凭证,查账时一目了然。
⚖️ 第二章:Swift 6 的新铁规 ------ 为何单例突然 "违法"?
虽说显式依赖注入是正道,但江湖总有 "不得不⽤单例" 的场景 ------ 比如 "系统时间工具"、"网络请求管理器",全 App 只能有一个实例,否则会出大问题。
可到了 Swift 6 这新江湖,连这种 "刚需单例" 都遭了殃。

韦爵爷照着老写法敲完代码,刚一运行,编译器就弹出一道刺眼的红牌,如同九门提督捕快举着的 "通缉令":
"Static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state"
(翻译过来就是:静态属性 "shared" 不安全!因为它是 "没靠山(未隔离)" 的全局可变状态)
韦爵爷气得直拍桌子:"我这'shared'用的是'let',又不是'var',怎么就'可变'了?编译器是不是跟我过不去?"
陈近南忙递上一杯茶,指着代码解释:"小宝莫急,你看这 AuthProvider 里的'memberTokens'------ 它是'var'类型,能随时修改。虽然'shared'本身是'let'(实例不变),但实例内部的状态能变啊!就像你手里的匕首(实例)不变,但匕首能捅人(内部状态修改),照样有风险。"
他又翻出骁骑营的 "战力账本" 代码,更是一目了然:
swift
// 骁骑营战力账本------用了 static var,直接遭红牌
class GamePiece {
// 编译器红牌:非并发安全!
static var power = 100 // 全局可变的战力值,谁都能改
}
"Swift 6 最看重'并发安全'," 陈近南继续说道,"它怕的是'多线程同时修改状态'------ 就像两个小兵同时改变战力值,一个加 10,一个减 5,最后账本上可能不是 105,而是 95 或者 110(数据竞态)。这种'暗箱操作',在新江湖里绝对不允许!"
韦爵爷这才恍然大悟:"原来不是编译器针对我,是这新规矩管得严!那我该怎么改?总不能不用单例吧?"

陈近南笑着摇头:"倒也不是不能用,只是得按新规矩来。要破这'非并发安全'的罪名,得分两种情况:一种是'实例内部状态不变'(比如纯工具类,只做计算不存数据),改个小写法就能过;另一种是'实例内部要变'(比如战力值、令牌库),就得给它找个'靠山'------ 要么投靠'全局演员'(MainActor),要么用'unsafe 免死金牌',还有更稳妥的'actor 独行侠'路子。"
韦爵爷听得眼睛发亮:"这么多门道?快给我说说第一种情况,怎么改个小写法就能过?"
陈近南却故意卖起关子:"小宝别急,这'改写法'虽简单,却藏着新坑 ------ 比如改成'static let'后,编译器可能又会甩出'非 Sendable 类型'的新罪名。而且'找靠山'的三种门道,各有优劣,得细细拆解才不会踩坑。咱们下篇就从'静态变量改常量'的解法说起,再对比'投靠 MainActor'和'用 unsafe 免死牌'的利弊,保准让你把第一重难关的出路摸得明明白白。"

欲知 "static let 如何化解第一道红牌","非 Sendable 新罪名又是什么来头",且看下篇分解。