为什么 Swift 禁止扩展里存值?
Swift 扩展(extension)不能增加存储属性,原因有三:
-
内存布局已确定
类/结构体的大小在编译期就固定,随意加字段会破坏 ABI。
-
类型安全
允许多个模块给同一类型加字段,会出现命名冲突、重复存储。
-
语言哲学
扩展应"加行为,不改状态"。如果必须加状态,说明设计该重构了。
方案 1:嵌套静态实体(纯 Swift,无 OC 依赖)
思路:把值存在全局/静态变量里,再用计算属性转发。
swift
class Object {}
extension Object {
// 1. 私有嵌套枚举,仅作命名空间
private enum Storage {
// ⚠️ 静态变量,所有实例共用一份
static var property: String = ""
}
// 2. 计算属性转发
var property: String {
get { Storage.property }
set { Storage.property = newValue }
}
}
适用场景
- 全局共享一份默认值(例如 App 配置)。
- 不想引入 Objective-C 运行时。
陷阱
- 所有实例共享同一变量 → 线程安全、命名冲突要自己保证。
- 不能实现"每个实例各自附加字段"。
方案 2:Associated Objects(每实例独立存储)
思路:借助 Objective-C 运行时,把"键-值"挂在对象上。
导入头文件
swift
import ObjectiveC
swift
extension Object {
private enum Key {
// 用 Void? 取地址即可,不占内存
static var propertyKey: Void?
}
}
计算属性封装
swift
extension Object {
var property: String? {
get {
objc_getAssociatedObject(self, &Key.propertyKey) as? String
}
set {
objc_setAssociatedObject(
self,
&Key.propertyKey,
newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
关联策略一览
策略 | 语义 | 常用场景 |
---|---|---|
.OBJC_ASSOCIATION_RETAIN |
原子 + strong | 线程安全对象 |
.OBJC_ASSOCIATION_RETAIN_NONATOMIC |
非原子 + strong | 性能优先,UI 控件 |
.OBJC_ASSOCIATION_COPY |
原子 + copy | NSString /NSArray 等可拷贝类型 |
.OBJC_ASSOCIATION_ASSIGN |
非拥有引用 | 弱关联(需手动置 nil) |
两种方案对比
维度 | 嵌套静态实体 | Associated Objects |
---|---|---|
是否纯 Swift | ✅ | ❌(需 ObjectiveC) |
每实例独立存储 | ❌(全局一份) | ✅ |
线程安全 | 自己加锁 | 依赖策略(原子/非原子) |
生命周期 | 与 App 相同 | 与对象生命周期绑定 |
性能 | 直接内存访问 | 哈希表查找,略慢 |
适用 | 共享配置、单例 | 给 UIView/VC 挂私有属性 |
实战:给 UIView 加一个"点击闭包"扩展
swift
import ObjectiveC
extension UIView {
private struct Key {
static var tapAction: Void?
}
var onTap: (() -> Void)? {
get {
objc_getAssociatedObject(self, &Key.tapAction) as? () -> Void
}
set {
objc_setAssociatedObject(
self,
&Key.tapAction,
newValue,
.OBJC_ASSOCIATION_COPY_NONATOMIC
)
}
}
}
// 使用
let button = UIButton()
button.onTap = { print("被点了一下") }
设计忠告:能不用就不用
-
优先考虑组合
把状态封装到专用对象里,而不是挂在别人身上。
-
命名空间要私有
嵌套 enum / struct 都标
private
,防止污染全局。 -
线程安全
静态变量用
NSLock
/DispatchQueue
;关联对象选对策略。 -
文档注释
说明"此属性通过扩展实现,生命周期与宿主对象一致"。
一句话总结
- 嵌套静态实体:简单、纯 Swift,但全局共享。
- Associated Objects:能给每个实例挂私货,却依赖 OC 运行时。
记住:
"扩展里存状态"是语言红线,出现这种需求时,先问自己"是不是该新建一个类型?"
如果答案是否,再安全地"曲线救国"。