本节摘要:本节讲类型系统的最后一块编制——协议定义"能做什么"的能力契约,扩展给任何已有类型追加实现,访问控制划定可见边界。结论先行:Swift 的复用主力是协议而非继承;协议加扩展的组合是 SwiftUI 数据流与 UIKit 委托模式的共同地基。
调度规则(3.2 节)管住了"是一种"的血缘关系,但剧团里还有大量"能力"关系:无论你是结构体还是类,只要"能出票",就能进结账通道。这种关系用契约——协议——来表达。
协议只列要求,不写实现。谁遵循它,谁就必须交齐清单上的每一项:
// 契约一:能展示为一行界面文案 protocol Summarizable { var oneLine: String { get } // get:至少可读(实现方可以加 set) } // 契约二:能参与购票,且要求先满足契约一(协议可继承协议) protocol Bookable: Summarizable { var seatsLeft: Int { get } mutating func reserve() -> Bool // mutating:允许值类型实现时修改自身 }
结构体和类都可以签约,这正是不受继承树约束的自由:
struct Session3: Bookable { let filmName: String var seatsLeft: Int var oneLine: String { "《\(filmName)》余 \(seatsLeft) 座" } mutating func reserve() -> Bool { guard seatsLeft > 0 else { return false } // 2.1 节的 guard 继续上岗 seatsLeft -= 1 return true } } final class GiftCard: Bookable { let balance: Int var seatsLeft: Int { balance } // 计算属性满足 get 要求 var oneLine: String { "礼品卡余额 \(balance) 张" } func reserve() -> Bool { balance > 0 } // 类是引用类型,实现 mutating 要求时不用写 mutating } var session = Session3(filmName: "雾中灯塔", seatsLeft: 2) print(session.oneLine) // 输出:《雾中灯塔》余 2 座 print(session.reserve()) // 输出:true print(session.seatsLeft) // 输出:1
面向协议编程的回报与多态同款:结账通道只认 Bookable,不关心背后是结构体还是类:
func checkout(_ items: [Bookable]) { for item in items { print("待出票:\(item.oneLine)") } } checkout([session, GiftCard(balance: 3)]) // 输出: // 待出票:《雾中灯塔》余 1 座 // 待出票:礼品卡余额 3 张

扩展(extension)能在类型定义之外、甚至对系统类型追加功能。它最漂亮的用法是把协议实现按功能分块组织:
// 给上一节的 Session3 补充能力:按功能分块的扩展 extension Session3 { // 追加计算属性 var urgency: String { seatsLeft <= 3 ? "仅剩少量" : "票源充足" } } // 给 String 扩展排片专用格式化——系统类型也能加戏 extension String { var withBrackets: String { "《\(self)》" } } print(session.urgency) // 输出:仅剩少量 print("星际列车".withBrackets) // 输出:《星际列车》
SwiftUI 的 API 全按这个思路铺开:你写的一堆修饰方法,本质都是对视图类型的扩展链。扩展也能直接提供协议默认实现(协议扩展),这是"模板方法"在 Swift 里的形态,第7章前可以先把"扩展=分块加戏"用熟。
⚠️ 常见坑:扩展里能加计算属性与方法,但不能加存储属性(结构体的存储布局在定义时就已冻结)。想在扩展里存状态,只能封装到别处或用第6章的属性包装器。
剧团大了要划后台边界。Swift 从宽到严四档:open(可跨模块继承,仅类)、public(跨模块可用)、internal(模块内可用,默认值)、fileprivate(仅本文件)、private(仅本类型与同文件扩展)。界面工程里最常用的是后三档:
struct BoardModel { private(set) var seatsLeft: Int // 外部可读,写入仅限内部 private var secretSalt = "xx" // 完全私有:外界不可见 init(seatsLeft: Int) { self.seatsLeft = seatsLeft } mutating func reserveOne() -> Bool { guard seatsLeft > 0 else { return false } seatsLeft -= 1 return true } } var board = BoardModel(seatsLeft: 5) print(board.seatsLeft) // 输出:5(可读) print(board.reserveOne()) // 输出:true print(board.seatsLeft) // 输出:4(只能经由内部方法变化) // board.seatsLeft = 0 // 编译错误:setter 是私有的
private(set) 是界面代码的常客:余票数对界面可读(渲染用),但只能通过 reserveOne 修改——状态变化的通道被收窄到一条,排查"票数怎么莫名变了"时只需要查一个方法。
背景:产品要求所有能上排片界面的条目(场次、礼包、会员卡)都能生成分享文案,且文案格式统一。操作:定协议、三方签约、通道统一调用:
protocol Shareable { func shareText() -> String } extension Shareable { // 协议扩展:默认实现,签约即得 func shareText() -> String { "来看这个:\(oneLineDefault)" } var oneLineDefault: String { "拾光影院好内容" } } extension Session3: Shareable { // 各自覆写出个性文案 func shareText() -> String { "《\(filmName)》就剩 \(seatsLeft) 座,手慢无" } } extension GiftCard: Shareable {} // 不覆写,用默认实现 let shareables: [Shareable] = [session, GiftCard(balance: 3)] for s in shareables { print(s.shareText()) } // 输出: // 《雾中灯塔》就剩 1 座,手慢无 // 来看这个:拾光影院好内容
结果解读:协议扩展提供了默认台词,需要个性的覆写、不需要的白拿——新增第四种条目时,一行签约就接入分享功能。变式:把默认实现挪进协议声明(不行——协议声明里不能有实现体,这正是协议扩展存在的理由),或给 Shareable 加 where Self: Bookable 约束(第6章泛型处细讲)。
💡 关键直觉:协议是 Swift 的"接口政治"——它让结构体和类站在同一条通道前平起平坐。第7章你会看到 SwiftUI 的 View 协议、UIKit 的一堆委托协议,全是这套机制的工程化应用。
剧团编制齐整了。下一幕转场到后台——那里有一本账,记着每个类对象被谁引用着;账本清零的瞬间,对象谢幕。第4章:ARC 内存管理。