2.6 协议与扩展:嫁接的接口面


2.6 协议与扩展:嫁接的接口面

本节摘要:协议声明"能做到什么"而不问"你是什么";扩展给任何既有类型凭空添能力。两者组合是 Swift 的嫁接术:一个结构体、一个类、一个枚举,只要实现同一协议,就能被同一份代码消费。本节实现阳台花园的养护规则协议 CareRule,看协议作为类型使用、协议扩展提供默认实现,并重构 2.5 的继承版提醒器——同一需求,两副骨架的重量差一目了然。

嫁接面:协议定义能力契约

上一节末尾留了句话:现代 Swift 更偏爱协议而不是继承。原因在耦合方式——继承把两个类型焊死成父子,协议只是约定一个接口面:谁实现,谁就算数。

// 协议:只声明要求,不提供存储 protocol CareRule { var ruleName: String { get } // 要求一个可读属性 func daysUntilWater(for plant: Plant) -> Int // 要求一个方法 } // 结构体实现协议:水分传感器派 struct SensorRule: CareRule { let ruleName = "传感器派" func daysUntilWater(for plant: Plant) -> Int { return plant.isThirsty ? 0 : plant.remainingDays } } // 类实现协议:经验派(顺带演示类实现协议时还能保有自己的继承) class VeteranRule: CareRule { let ruleName = "经验派" func daysUntilWater(for plant: Plant) -> Int { // 经验:间隔减半再等一天,宁早勿晚 return max(plant.intervalDays / 2 - plant.passedDays, 0) } } // 枚举也能实现协议——三种类型同台 enum LazyRule: CareRule { case weekendOnly var ruleName: String { "懒人派" } func daysUntilWater(for plant: Plant) -> Int { return plant.isThirsty ? 0 : 7 // 憋到周末一起浇 } }

协议真正的威力在"当类型用":一个数组、一个参数,能同时容纳三种毫不相干的类型——凭据不是血统,是契约:

let rules: [CareRule] = [SensorRule(), VeteranRule(), LazyRule.weekendOnly] let testPlant = Plant(name: "龟背竹", kind: .foliage, intervalDays: 6, passedDays: 2) for rule in rules { print("\(rule.ruleName):\(rule.daysUntilWater(for: testPlant)) 天后浇水") } // 输出: // 传感器派:4 天后浇水 // 经验派:1 天后浇水 // 懒人派:7 天后浇水 let fastest = rules.min { $0.daysUntilWater(for: testPlant) < $1.daysUntilWater(for: testPlant) } print("采纳:\(fastest?.ruleName ?? "无")") // 采纳:经验派

图 2-5 嫁接面:一个协议,三种砧木

图 2-5 嫁接面:一个协议,三种砧木

扩展:给既有类型添枝

扩展(extension)不问类型的出身,直接往上嫁新能力——哪怕是 Int、String 这种标准库类型:

// 给 Int 嫁接"天数"表达能力 extension Int { var days: Int { self } // 3.days 比裸的 3 可读 func shorterThan(_ other: Int) -> Bool { self < other } } print(3.days) // 3 print(3.shorterThan(7)) // true // 给 Plant 嫁接展示能力(模型是 2.4 定义的结构体,一行没改它) extension Plant { var badge: String { "\(name)·\(kind.rawValue)" } } print(testPlant.badge) // 龟背竹·观叶 // 协议扩展:给契约本身提供默认实现——实现者白得,不要可覆盖 extension CareRule { var ruleName: String { "通用规则" } // 默认名 func urgency(for plant: Plant) -> String { daysUntilWater(for: plant) == 0 ? "紧急" : "常规" } } struct SimpleRule: CareRule { // 只实现方法,名字白得默认值 func daysUntilWater(for plant: Plant) -> Int { plant.remainingDays } } let simple = SimpleRule() print(simple.ruleName) // 通用规则 print(simple.urgency(for: testPlant)) // 常规

协议扩展是标准库泛型算法的底层机关,日常最常见用法就是给协议塞默认实现,让实现者按需只写差异部分。

案例:提醒服务的协议化重构

背景:2.5 节用继承做了 ReminderService 与 ThirstyReminder,父类改一处、子类全线受牵连,且"不是提醒器的类型"永远进不了那个数组。操作:改用协议 + 协议扩展默认实现。结果如下:

protocol Notifier { var channel: String { get } func message(for plant: Plant) -> String } extension Notifier { // 默认渠道名 + 默认通知格式 var channel: String { "站内" } func send(for plant: Plant) -> String { "【\(channel)】\(message(for: plant))" } } struct WaterNotifier: Notifier { let channel = "推送" func message(for plant: Plant) -> String { plant.isThirsty ? "\(plant.name) 缺水" : "\(plant.name) 正常" } } struct FertilizerNotifier: Notifier { // 只写差异:两行搞定一个新通知器 func message(for plant: Plant) -> String { "\(plant.name) 该施肥了" } } let notifiers: [Notifier] = [WaterNotifier(), FertilizerNotifier()] for n in notifiers { print(n.send(for: testPlant)) } // 输出: // 【推送】龟背竹 正常 // 【站内】龟背竹 该施肥了

解读:对照继承版——新增类型不再需要与父类建立血统关系,默认实现放协议扩展里,想覆盖就自己写;两种 Notifier 一个是带定制的结构体、一个是全默认的结构体,调用方毫无感知。变式:给协议加关联类型或用泛型约束(2.8)可以把"能比较的通知器"再分一层;第 4 章 UITableView 的数据源协议、第 5 章 SwiftUI 的 View 协议,都是这套模式在框架级的应用——你此刻已经在用苹果框架的同款思想。

💡 关键直觉:继承回答"你爹是谁",协议回答"你会什么"。工程上后一个问题几乎总是更有用。

本节要点回顾

  • 协议是能力契约:声明属性与方法要求,不提供存储,任何类型都能实现;
  • 协议可当类型用:异构集合、统一参数,凭契约不凭血统;
  • 扩展能给任何类型(含 Int/String)嫁接能力,也是给模型补展示逻辑的无侵入手段;
  • 协议扩展提供默认实现,实现者白得、可按需覆盖;
  • 选型口诀:is-a 考虑继承,can-do 用协议。

能力面有了,还差最后的防护与自动化——下一节处理"没有值"这件小事。


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