3.1 演员名册:类、结构体与枚举


3.1 演员名册:类、结构体与枚举

本节摘要:本节给排片业务立正式编制:结构体 Session 承载数据、枚举 Hall 承载状态、类承载需要共享身份的调度者;并用对照实验讲清值语义与引用语义的差别。结论先行:默认用结构体,出现"多个界面要共享同一个会变的东西"时才考虑类——这个选择决定了状态变化如何传播。

第一幕里界面还靠字典硬撑(2.3 节的 fetchBoard 返回字符串数组),这一幕开改革动员会:每个业务概念领一个正式类型。

名册改革动员会

先看改革前的痛。加载回来的"一场放映"是这样一个字典:

let raw: [String: Any] = [ "film": "雾中灯塔", "hall": "IMAX", "minutes": 118, "seats": 12, ] let film = raw["film"] as? String ?? "" // 每取一个字段都要 as? 一次 let minutes = raw["minutes"] as? Int ?? 0 print("\(film) \(minutes) 分钟") // 输出:雾中灯塔 118 分钟

键名打错(film 写成 flim)没有编译错误,只有运行时的空值兜底;类型靠 as? 现场盘问;业务规则(余票为零不可订)无处安放。改革的做法是把它变成结构体:

// 一场放映:数据与规则住在一起 struct Session { let filmName: String // 片名:定档后不变 var hall: Hall // 厅:可能临时调整 let minutes: Int // 片长 var seatsLeft: Int // 余票:随销售变化 // 计算属性:由已有字段推导,不占存储 var isAlmostFull: Bool { seatsLeft <= 5 } // 方法:业务规则封装在类型内部 func summary() -> String { "《\(filmName)》\(hall.displayName) 余 \(seatsLeft) 座" } }

枚举负责"状态"这种角色。放映厅是有限集合,比字符串更适合枚举:

enum Hall { case standard, imax, dolby // 三种厅,编译器担保不会有第四种拼写错误 var displayName: String { switch self { case .standard: return "标准厅" case .imax: return "IMAX 厅" case .dolby: return "杜比厅" } } } var session = Session(filmName: "雾中灯塔", hall: .imax, minutes: 118, seatsLeft: 4) print(session.summary()) // 输出:《雾中灯塔》IMAX 厅 余 4 座 print(session.isAlmostFull) // 输出:true

结构体自带"成员逐一初始化器"(编译器免费送),声明时不必手写 init;一旦你手写任何初始化器,免费的就收走。

值与引用:一场决定性的对照实验

结构体与类语法几乎一样,差别藏在"赋值那一刻"。这是全节最重要的一段代码:

// 结构体实验:复制传播 var a = session a.seatsLeft = 30 // 改的是副本 print(session.seatsLeft) // 输出:4 —— 原件安然无恙 print(a.seatsLeft) // 输出:30 —— 副本自说自话 // 类实验:共享传播 final class TicketDesk { var openHalls: Int init(openHalls: Int) { self.openHalls = openHalls } } let deskA = TicketDesk(openHalls: 3) let deskB = deskA // 复制的是"引用",不是对象 deskB.openHalls = 5 print(deskA.openHalls) // 输出:5 —— 两个名字,同一个对象

两次赋值,两种世界。结构体赋值像复印剧本:每人一份,各自涂改互不影响;类赋值像对讲机频道:两个名字指向同一现场,谁喊都改变同一份状态。

值语义与引用语义:赋值那一刻分岔

值语义与引用语义:赋值那一刻分岔

变式:把 deskB 声明成 var deskB = deskA 再改属性,结果一样——引用语义与 var/let 无关,let 锁的是"名字不能改绑",锁不住对象内部属性(除非把属性也声明成 let)。

何时用枚举:状态就该枚举

余票是数量(Int),但"这场放映处于什么阶段"是状态。状态用枚举,编译器能强制你处理每种情况:

enum SessionPhase { case scheduled(minutesToOpen: Int) // 已排片:距开售还有几分钟(关联值) case onSale // 售票中 case soldOut // 已售罄 var label: String { switch self { case .scheduled(let m): return "\(m) 分钟后开售" case .onSale: return "售票中" case .soldOut: return "已售罄" } } } let phase: SessionPhase = .scheduled(minutesToOpen: 30) print(phase.label) // 输出:30 分钟后开售

两个细节值得点名。其一,case 之间用换行(或逗号)分隔——从某些语言迁移过来的写作者常误用竖线分隔,Swift 编译器会当场拒绝。其二,switch 匹配枚举时若漏掉任何一个 case 且不写 default,编译器直接报错——这正是枚举的价值:将来新增一种状态(比如"点映"),所有漏掉处理的代码会同时变红,逼你补戏。

⚠️ 常见坑一:结构体方法里直接改属性会报错,需要 mutating func 标记(值类型复制语义下的自我声明)。坑二:把"界面要共享的可变状态"建成结构体,又在两处分别修改,然后奇怪为什么不同步——那是复印剧本的世界,共享请走类或第7章的状态对象。

💡 关键直觉:选型口诀——数据用结构体(复制),状态集合用枚举(穷举),需要共享身份或生命周期管理才用类(引用)。Apple 全家桶里 String、Array、Int 全是结构体,类是少数派。

改革验收

  • 结构体:免费成员初始化器、计算属性、方法,值语义复制传播;
  • 枚举:有限状态 + 关联值携带上下文,switch 穷举受编译器保护;
  • :引用语义共享同一对象,final 阻止继承(第3.2 节);
  • 赋值实验是判断语义的最快手段:改副本看原件;
  • mutating 是值类型方法的自我修改声明。

名册有了,下一节排调度:一个点击事件进来,普通场次、会员场次、首映场次要走出不同流程——继承与多态负责让"同一句指令,各自表演"。


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