3.2 上场调度:继承多态与类型转换


3.2 上场调度:继承、多态与类型转换

本节摘要:本节讲类家族的调度术——继承复用父类骨架、覆写定制行为、多态让同一指令各自表演,以及 as?、as!、is 三种类型转换。结论先行:继承表达"是一种"的血缘,多态依赖动态派发在运行时选对演员;转换务必用 as? 试探,as! 只留给已知必然成功的场合。

名册(3.1 节)立好了,调度室开工:同一个"出票"指令,普通票、会员票、首映票要走出三条流程。这一节就是调度室的工作记录。

调度室第一课:继承与覆写

先建父类,把三种子票共有的骨架放进去:

// 父类:一张票的通用骨架 class Ticket { let filmName: String var price: Double init(filmName: String, price: Double) { self.filmName = filmName self.price = price } // 父类给默认台词,子类可覆写 func receiptLine() -> String { "《\(filmName)》票价 \(price) 元" } } // 子类一:会员票——血缘上"是一种票" final class VipTicket: Ticket { let discount: Double init(filmName: String, price: Double, discount: Double) { self.discount = discount super.init(filmName: filmName, price: price * (1 - discount)) // 先安顿好自己,再交给父类 } // 覆写:同一段台词,念出自己的味道 override func receiptLine() -> String { "【会员 \(Int(discount * 100)) 折】" + super.receiptLine() // super 调父类版本 } } // 子类二:首映票——附赠映后谈 final class PremiereTicket: Ticket { let talkMinutes: Int init(filmName: String, price: Double, talkMinutes: Int) { self.talkMinutes = talkMinutes super.init(filmName: filmName, price: price) } override func receiptLine() -> String { "【映后谈 \(talkMinutes) 分钟】《\(filmName)》票价 \(price) 元" } }

两个规则要记牢:覆写必须写 override,漏写编译器报错(防手滑遮蔽父类方法);子类初始化器必须先把自有属性赋值,再调 super.initfinal 标记"到此为止不许再继承"——不打算被继承的类都该标上,编译器还能因此做性能优化(跳过动态派发)。

调度室第二课:多态,同一指令各自表演

多态是继承的回报时刻。父类类型的数组里混装子类实例,调用同名方法,运行时各自执行自己的版本:

let tickets: [Ticket] = [ Ticket(filmName: "深海回声", price: 45), VipTicket(filmName: "雾中灯塔", price: 60, discount: 0.2), PremiereTicket(filmName: "星际列车", price: 80, talkMinutes: 25), ] for ticket in tickets { print(ticket.receiptLine()) // 同一句指令,三种表演 } // 输出: // 《深海回声》票价 45.0 元 // 【会员 20 折】《雾中灯塔》票价 48.0 元 // 【映后谈 25 分钟】《星际列车》票价 80.0 元

静态看,tickets 里住的都是 Ticket;动态看,运行时按每个元素的实际类型挑方法版本——这个机制叫动态派发。价值在于:调度代码(for 循环)完全不知道子类的存在,新增一种票,循环一个字都不用改。

⚠️ 常见坑:把"组件"误建成继承链。比如给"会员票"和"会员用户"找一个共同父类——它们没有"是一种"的血缘,硬凑父类会得到一堆空方法。没有血缘的复用请走 3.3 节的协议;结构体则根本不能继承,这是有意的设计。

调度室第三课:类型转换的三个手势

装在父类数组里的子类,某天要用子类独有功能(比如会员票的 discount),就得先验明正身:

// 手势一:is —— 只问身份 for ticket in tickets where ticket is VipTicket { print("发现一张会员票") } // 输出:发现一张会员票 // 手势二:as? —— 试探转换,失败得 nil(可选值第 1.2 节的又一次上岗) if let vip = tickets[1] as? VipTicket { print("该票优惠了 \(Int(vip.discount * 100)) 折") } else { print("这不是会员票") } // 输出:该票优惠了 20 折 // 手势三:as! —— 强转,失败即崩溃 let first = tickets[0] // let wrong = first as! VipTicket // 运行时崩溃:它明明是普通票 let surelyVip = tickets[1] as! VipTicket // 我们刚刚验证过 tickets[1],此刻才敢强转 print(surelyVip.discount) // 输出:0.2

三个手势的风险梯度与 1.2 节解包完全同构:is 零风险、as? 返回可选、as! 赌上运行时。还有一个向上转换的恒等手势:任何子类实例都能直接 as Ticket 用作父类,永远不会失败。

完整案例:一张会员票进错通道

背景:影院检票口按"是否需要核验会员码"分两条通道,检票函数收到的都是 Ticket 类型。操作:先 is 分流,再 as? 取会员信息:

func checkIn(_ ticket: Ticket) { if let vip = ticket as? VipTicket { print("会员通道:核验会员码,优惠 \(Int(vip.discount * 100)) 折") } else if ticket is PremiereTicket { print("首映通道:发放映后谈手环") } else { print("普通通道:扫码进场 —— \(ticket.receiptLine())") } } checkIn(tickets[0]) // 输出:普通通道:扫码进场 —— 《深海回声》票价 45.0 元 checkIn(tickets[1]) // 输出:会员通道:核验会员码,优惠 20 折 checkIn(tickets[2]) // 输出:首映通道:发放映后谈手环

结果解读:检票函数对外只认 Ticket,内部用转换手势识别具体身份——这就是"面向父类编程、按子类处理细节"的标准姿势。变式:把 PremiereTicket 的分支挪到 VipTicket 前面,行为不变(两者互斥);但如果将来出现"会员首映票"这种多重身份,if-else 链会开始打架,那时就该重构成 3.3 节的协议方案。

💡 关键直觉:继承的判断题只有一道——"子类是不是父类的一种,且永远会是"。答案犹豫,就别继承。Swift 标准库里继承树极浅,协议才是这个语言的主旋律。

调度收工

  • 继承: 父类,覆写必写 override,初始化先安顿自有属性再交棒 super;
  • final 封死继承链,兼得性能红利;
  • 多态靠动态派发按实际类型执行,调度代码对子类无感知;
  • is / as? / as! 三手势分别对应"问、试、赌",与可选值解包同构;
  • 无血缘的复用走协议——这正是下一节的主题。

调度规则定了,但剧团大了会有新的烦恼:怎么规定"凡是能上台的,都必须会报幕"?3.3 节签契约——协议、扩展与访问控制。


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