2.5 类与引用语义:共享的树干


2.5 类与引用语义:共享的树干

本节摘要:结构体靠分裂繁衍,类靠共享成林。本节讲类的声明与初始化器、继承与方法重写、多态的运行时分发,以及引用语义的两面性:改一处、处处见——既是"全局状态同步"的便利,也是"误伤远处代码"的事故源。判据收尾:什么时候该用类(需要共享身份、需要继承、需要 Objective-C 互操作),什么时候坚决用结构体。

同一根树干:引用的共享性

上一节的结构体世界里没有"同一个"的概念;但界面工程里有些东西天生就是独一份的:一个数据仓库、一个会话、一个控制器。类就是为"独一份"而生的:变量里存的不是对象本身,而是指向它的地址——两份变量名,一个实体。

// 类的声明:没有免费初始化器,必须自己写(全属性都要照顾到) class PlantStore { var plants: [Plant] = [] // 可变状态:仓库里的植物清单 let created: String // 不可变身份属性 init(created: String) { // 指定初始化器 self.created = created print("仓库开张:\(created)") } func add(_ plant: Plant) { plants.append(plant) } func waterAll() { for i in plants.indices { plants[i].water() } // 逐株浇水,重置计数 } func thirstyCount() -> Int { plants.filter { $0.isThirsty }.count } } let store = PlantStore(created: "阳台花园主仓库") // 仓库开张:阳台花园主仓库 store.add(Plant(name: "龟背竹", kind: .foliage, intervalDays: 3)) store.add(Plant(name: "多肉", kind: .succulent, intervalDays: 10)) let alias = store // 注意:没有 & ,没有 copy——这行只是多贴了一张名牌 alias.add(Plant(name: "绿萝", kind: .fern, intervalDays: 2)) print(store.plants.count) // 3:从 store 看,绿萝已经在列——因为两个名字指同一实体 print(store === alias) // true:同一性比较,类专属

=== 比较的不是内容而是身份:"是同一个实体吗"。这份同一性就是共享的基础,也是一切麻烦的起点。

图 2-4 引用共享:两个名牌指向同一根树干

图 2-4 引用共享:两个名牌指向同一根树干

继承与多态:主干分出侧枝

类的第二能力是继承:子类获得父类全部能力,再按需重写。阳台花园的提醒服务演示完整链条:

class ReminderService { var label: String { "普通提醒" } func message(for plant: Plant) -> String { return "\(plant.name):按计划养护" } // final 封死重写:设计上不允许子类改行为时显式声明 final func notify(for plant: Plant) -> String { return "【\(label)】\(message(for: plant))" } } class ThirstyReminder: ReminderService { // 侧枝:专注缺水场景 override var label: String { "缺水警报" } // 重写计算属性 override func message(for plant: Plant) -> String { return plant.isThirsty ? "\(plant.name):叶子都耷拉了,快浇!" : "\(plant.name):水分充足" } } let services: [ReminderService] = [ReminderService(), ThirstyReminder()] for svc in services { // 多态:同一个调用,各走各的实现 print(svc.notify(for: store.plants[1])) } // 输出: // 【普通提醒】多肉:按计划养护 // 【缺水警报】多肉:水分充足

运行时按"真实类型"选实现,就是多态。它换来了扩展性——新增一种"施肥提醒"服务,不改 notify 的调用方代码。但代价同样真实:继承层次深了以后,改父类会波及所有子类,重写链条越长越难追。Swift 社区的现代共识是能用协议(下一节)就用协议,继承留给确有"is-a"关系的场景

⚠️ 常见坑:引用共享最经典的事故是"顺手改了传进来的对象"。函数参数收到一个类实例,方法里直接改它的属性,调用方的原对象就变了——编译器毫无怨言。防御做法:改前问自己"这个方法有没有资格动这根树干";没有把握,把参数声明成 let 也拦不住属性修改(let 只锁名字不锁实体内容),要么传结构体副本,要么在类型设计上把可变面收窄。

案例:谁把多肉的记录改没了

背景:团队里两人分别写"统计"与"编辑"模块,共用同一个 store。上线后发现统计页偶尔把 passedDays 清零。操作:复现最小案例——统计模块图省事直接调了 waterAll。

func renderStatistics(of store: PlantStore) { store.waterAll() // 事故现场:为了"演示初始状态"顺手全浇了 print("统计时缺水株数:\(store.thirstyCount())") } renderStatistics(of: store) // 统计时缺水株数:0 print("主界面再看缺水株数:\(store.thirstyCount())") // 主界面再看缺水株数:0(被污染)

结果:统计跑完,全 App 的缺水数永远归零。解读:结构体传值是"抄一份给你改",类传引用是"原件递过去"——统计模块拿的是原件,改的是原件。变式(正确写法):统计只读不写,方法签名收一个纯函数式的快照;或仓库提供只读视图,把可变操作收进单独的编辑服务。第 5 章的 ObservableObject 会以"单一事实源"的纪律重新管住这根树干,第 6 章排错手册把"引用被误改"列为高发病之一。

本节要点回顾

  • 类存地址不存实体:赋值即共享名牌,=== 判同一性;
  • 初始化器必须自己写,全属性照料到位才能出生;
  • 继承 + 重写 + 多态换来扩展性,代价是耦合扩散,final 可封死;
  • 共享是双刃剑:全局同步的便利与远处误伤的风险同源;
  • 选型判据:要身份共享、要继承、要与旧框架互操作才用类;纯数据一律结构体。

侧枝靠继承太重,协议给出更轻的嫁接面——下一节。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U