第17章 结构体与值语义:复制之后互不打扰
4 分钟阅读
第十七章:结构体与值语义:复制之后互不打扰
结构体是 Swift 写业务模型时最常拿起的工具。原因不是它“比类快”这种口号,而是它默认带来清晰的复制语义:把值交给别人之后,对方改自己的副本,不会顺手改坏你手里的那份。
17.1 定义与成员逐一初始化
定义一个结构体,要做的只是把它的存储属性列出来:
| |
结构体自动获得成员逐一初始化器 init(x:y:),参数顺序和属性声明顺序一致。只要没有自定义初始化器,编译器就会生成它。
这个初始化器还能“变形”:属性有初值时对应参数会带默认值,全部属性都有初值时 Point() 直接可用,而 private 属性会改变它的可见性。这些变化连同只读计算属性、观察器隐式参数名、Self 等写法,集中在第 15B.6 节。
17.2 值语义:赋值得到新的一份
上面的定义里一个字都没提“复制”,但复制确实发生了。看这段:
| |
b = a 之后,b 拥有独立的一份值。修改 b 不会影响 a。函数传参时也一样:
| |
参数 point 是常量,不能直接改它的属性;想改就先复制成 var,或者把方法写成 mutating。
17.3 mutating 方法:原地修改值
结构体方法只有标记为 mutating 才能修改属性:
| |
为什么需要 mutating?因为结构体方法默认把 self 当成不可变值。mutating 明确告诉编译器:调用这个方法会改变实例本身。
常量实例不能调用 mutating 方法:
| |
17.4 常量结构体与可变属性的关系
结构体实例声明为 let 后,即使属性是 var,也不能修改:
| |
这和类不同。类实例的 let 只锁住引用本身,不锁住对象的属性。结构体的 let 锁住的是整个值。
17.5 自定义初始化器
自定义初始化器会替代自动生成的成员逐一初始化器:
| |
如果还想要成员逐一初始化器,可以把它放到扩展里,或者不自定义初始化器。初始化器不能像普通方法那样用 .init 链随意转弯,必须保证所有存储属性在结束时都有值。
17.6 嵌套的值语义
结构体嵌套结构体时,复制会沿着整棵值树进行:
| |
值语义会一路复制,修改 r2 内外层都不会影响 r1。
17.7 结构体里放类的陷阱
结构体是值类型,不代表它里面的所有东西都会被值复制:
| |
Holder 被复制了,但复制的是同一个 Box 引用。两个结构体各自持有一个引用,却指向同一个对象。写值类型模型时,看见引用类型属性就要多想一步。
17.8 结构体适合什么
结构体最适合:
- 坐标、尺寸、范围、配置这类小而独立的数据。
- 需要值语义、可安全复制的数据。
- 作为模型值传递,不希望调用方共享可变状态。
类更适合:
- 有明确身份、生命周期和共享引用需求的实体。
- 需要继承和运行时多态的对象图。
- 需要
deinit清理资源的对象。
SwiftUI 的状态模型大量使用结构体,正是因为“重现一个值”比“追踪一个被改来改去的对象”容易得多。
17.9 本章小结
| 主题 | 关键结论 |
|---|---|
| 自动初始化器 | 属性没有默认值时生成成员逐一初始化器 |
| 值语义 | 赋值、传参得到独立副本 |
mutating | 结构体修改自身属性的方法必须标注 |
let 实例 | 整个结构体值不可变 |
| 自定义初始化器 | 可能替代自动生成的成员逐一初始化器 |
| 引用属性 | 结构体复制时,类属性复制的是引用 |
17.10 本章易错点速查
| 易错点 | 正确理解 |
|---|---|
| 以为结构体永远没有共享 | 内嵌类属性仍会共享对象 |
在方法里直接改 var 属性 | 必须把方法声明为 mutating |
以为 let 属性不能有变化 | let 锁整个值,属性级别可读不可改 |
| 自定义初始化器后找不到成员逐一初始化器 | 自定义会替代自动生成,必要时放扩展 |
| 结构体和类混着建模却不区分语义 | 先问“这是值还是身份”,再选类型 |
17.11 下章预告
下一章讲类。它带来共享引用、继承和生命周期,也带来别名、循环引用等必须负责的后果。结构体给你独立副本,类给你身份;两者都不是“更高级”的替代。