写到这里,曾怀疑 Singleton 真的可以一直这样用的人
做 iOS 开发时,应该没有人从没用过 Singleton。我也一样。SomeManager.shared 这种代码在每个项目里至少都会出现好几处。
但从某个时候开始,这个.shared逐渐变得让人不舒服。测试时状态会互相干扰,团队成员也会从各处随意访问它。
后来我了解到 Monostate 模式。今天想分享我在 Swift 中使用 Monostate 的亲身体验,以及它是否真的能成为 Singleton 的替代方案。
先说结论:Monostate 是一种让状态保持共享、同时允许创建多个实例的模式。它和 Singleton 的目标相同,但实现方式完全相反。不过它不是万能方案,还是要根据具体情况选择。下面逐一展开。
Monostate 模式到底是什么?
Singleton 的核心是“只创建一个实例”。通过限制初始化器,只使用shared。
Monostate 正好相反。实例可以创建任意多个,但其中的状态(数据)全部通过 static 共享。
也就是说,无论创建对象 A 还是 B,它们看到的数据都来自同一个地方。表面上像普通对象,内部却连接着同一份状态。
用 Swift 简单表示,大概是下面这样。
struct Settings {
private static var _volume = 50 // 共享 static状态
var volume: Int { // 像实例一样使用
get { Settings._volume }
set { Settings._volume = newValue }
}
}
// 实例不同,但状态只有一份
var a = Settings(); a.volume = 80
print(Settings().volume) // 80
即使新建Settings(),volume 仍然是 80。实例有多个,但状态只有一份,这就是 Monostate 的全部。
它和 Singleton 有什么不同?
最大的区别在于使用方的代码。
使用 Singleton 时,调用方必须始终意识到.shared,并想着“这是 Singleton”。使用 Monostate 时,只需像使用普通对象一样创建Settings(),不需要了解内部实现。
整理成表格如下。
| 分类 | Singleton | Monostate |
|---|---|---|
| 实例数量 | 只有 1 个 | 可以有多个 |
| 共享内容 | 实例本身 | 状态(static 数据) |
| 使用方 | 显式指定.shared |
像普通对象一样 |
| 继承 | 麻烦 | 相对自由 |
尤其是继承很有意思。Singleton 很难继承,而 Monostate 是普通类型,因此扩展起来更加灵活。
那么,它能成为 Singleton 的替代方案吗?
说实话,有时可以,有时不行。
先说优点。使用方的代码更简洁,.shared这个全局访问点也不会散落在代码各处。由于接口和普通对象完全一样,之后调整结构时负担也更小。
Monostate 是一种“隐藏全局状态”的模式。看起来很方便,但隐藏的全局状态本身就是一把双刃剑。
问题就在这里。它表面上像普通对象,团队成员可能以为“这只是一个局部对象”而创建它,结果却发现状态其实是全局共享的,反而更容易混乱。
而且由于状态通过 static 共享,在多线程环境中仍然要同样注意并发问题。这一点和 Singleton 没有区别。
根据我的经验,在现代 Swift 中,我建议优先考虑依赖注入,而不是 Monostate。这样更容易测试,依赖关系也更清晰。
那么,什么时候适合使用呢?
总结来说,适合以下情况。
- 现有 Singleton 代码太多,想逐步移除时
- 想让使用方接口保持像普通对象一样简洁时
- 需要处理必须支持继承或扩展的全局状态时
反过来,如果是从零开始编写新项目,我会建议先考虑依赖注入或显式状态管理,而不是 Monostate。
归根结底,Monostate 不是“Singleton 的完美上位替代”,而是“性格不同的另一个选择”。把它放进工具箱,在适合的时候拿出来使用即可。
希望今天的内容能给那些厌倦.shared滥用的人提供一点小提示。与其盲目切换,不如认真想想什么最适合我们的项目。

