iOS Swift 值语义与引用语义
iOS Swift 值语义与引用语义
Swift 同时提供结构体、枚举和类。选择值类型还是引用类型,不只是语法偏好,它会影响状态共享、并发安全、复制成本和接口可读性。值语义强调每份数据独立变化,引用语义强调多个使用者观察同一个对象。先明确业务语义,再考虑性能,通常能得到更稳定的设计。
一、值类型意味着独立值
结构体赋值后,修改新变量不会改变旧变量:
struct UserProfile {
var nickname: String
var city: String
}
var original = UserProfile(nickname: "小林", city: "上海")
var editing = original
editing.nickname = "林林"
print(original.nickname)
print(editing.nickname)
这种行为适合表单草稿、请求参数、配置快照和界面展示模型。使用者不需要追踪“还有谁持有同一个对象”。
二、引用类型表示共享身份
类实例赋值后,两个变量指向同一个对象:
final class PlaybackSession {
var position: TimeInterval = 0
}
let firstReference = PlaybackSession()
let secondReference = firstReference
secondReference.position = 12
print(firstReference.position)
播放会话、数据库上下文、系统控制器等具有唯一身份和生命周期的对象,适合引用语义。类还支持继承和析构逻辑,但也需要处理共享可变状态和循环引用。
三、不要仅按数据大小选择
“结构体小、类大”是过度简化。真正的问题是数据是否需要身份,以及修改是否应该被其他持有者立即观察。
可以用几个问题判断:
- 两个内容完全相同的实例是否仍应被视为不同对象。
- 修改一个变量时,其他变量是否应该同步变化。
- 对象是否需要明确的创建和销毁生命周期。
- 是否需要在多个位置共享可变状态。
- 是否需要继承现有类体系。
答案更偏向独立快照时使用值类型,更偏向唯一身份时使用引用类型。
四、理解写时复制
Swift 标准集合具有值语义,但实现通常采用写时复制。多个变量读取同一份集合时可以共享底层存储,某一方修改时才真正复制。
var first = Array(0..<1_000)
var second = first
second.append(1_000)
赋值本身不一定立即复制全部元素,但修改可能触发较大成本。高频修改大型集合时,应通过测量确认复制行为,而不是因为它是值类型就假设没有开销。
五、自定义写时复制容器
需要同时提供值语义和共享存储时,可以用内部引用对象实现写时复制:
private final class Storage {
var values: [Int]
init(values: [Int]) {
self.values = values
}
func copy() -> Storage {
Storage(values: values)
}
}
struct NumberBuffer {
private var storage: Storage
init(values: [Int]) {
storage = Storage(values: values)
}
mutating func append(_ value: Int) {
if !isKnownUniquelyReferenced(&storage) {
storage = storage.copy()
}
storage.values.append(value)
}
}
这种实现需要保证所有修改入口都执行唯一性检查,否则外部看到的值语义会被破坏。普通业务模型没有必要自行实现复杂存储。
六、不可变属性让语义更明确
结构体内部尽量使用 let,通过创建新值表达状态变化:
struct OrderState: Equatable {
let orderId: String
let status: Status
let updateTime: Date
func updating(status: Status, at time: Date) -> OrderState {
OrderState(
orderId: orderId,
status: status,
updateTime: time
)
}
}
不可变快照更容易比较、记录和跨线程传递,也能避免某个模块无意修改共享数据。
七、协议中的 mutating 语义
值类型方法需要修改自身时,要使用 mutating:
protocol Resettable {
mutating func reset()
}
struct SearchCondition: Resettable {
var keyword = ""
var category = "all"
mutating func reset() {
self = SearchCondition()
}
}
这使调用方能够从方法签名看出操作会改变值。类实现同一协议时不需要 mutating,因为类实例本身通过引用修改。
八、闭包捕获中的差异
闭包捕获引用类型时,可能延长对象生命周期并形成循环引用。捕获可变值类型时,则要理解闭包何时复制、变量是否被装箱共享。
不要机械地给所有闭包添加弱捕获。应先确认闭包是否逃逸、由谁持有,以及目标对象在回调执行时是否应该继续存在。
九、并发环境中的选择
不可变值通常更容易在并发任务之间传递,因为任务拿到的是稳定快照。共享可变类则需要隔离或同步,防止多个执行上下文同时读写。
值类型也不是天然线程安全。如果内部包含引用存储,或者多个任务通过外部容器共同修改同一个变量,仍然会产生数据竞争。应遵循项目当前并发模型和编译器检查规则。
十、常见选择建议
- 业务数据、请求参数、界面模型优先考虑结构体。
- 有限状态集合优先考虑枚举。
- 具有身份、生命周期或共享行为的对象使用类。
- 需要继承系统控制器或视图时使用类。
- 需要资源释放回调时使用类,并明确所有权。
这些是起点而不是绝对规则。现有项目若已经围绕某种模型建立成熟约定,应优先保持一致。
总结
值语义让数据变化局部化,引用语义让多个使用者共享同一身份。根据“是否需要身份”和“修改是否需要共享”选择类型,再关注复制成本、生命周期和并发隔离,可以让 Swift 模型更容易理解和维护。
- 点赞
- 收藏
- 关注作者
评论(0)