协议与面向协议编程
问题
Swift 中的 Protocol 是什么?面向协议编程(POP)与面向对象编程(OOP)有什么区别?协议扩展有哪些用法?
答案
协议基础
协议定义了一套行为契约,遵循协议的类型必须实现所有要求的属性和方法:
protocol Drawable {
// 属性要求(必须指定 get / get set)
var color: String { get set }
// 方法要求
func draw()
// 静态方法要求
static func defaultColor() -> String
}
// struct 遵循协议
struct Circle: Drawable {
var color: String
var radius: Double
func draw() {
print("Drawing circle with radius \(radius)")
}
static func defaultColor() -> String { "red" }
}
协议 vs 抽象类(OC 的 class)
| 特性 | Protocol | 抽象类(class) |
|---|---|---|
| 多遵循 | ✅ 支持多个协议 | ❌ 单继承 |
| 默认实现 | ✅ 通过 extension | ✅ 直接在类中 |
| 存储属性 | ❌ 只能要求(计算属性通过扩展) | ✅ |
| 值类型支持 | ✅ struct/enum 都可遵循 | ❌ 仅 class |
| 初始化器 | ✅ 可要求 init | ✅ |
协议扩展——默认实现
协议扩展是 POP 的核心,提供默认实现而非仅定义接口:
protocol Greetable {
var name: String { get }
func greet() -> String
}
extension Greetable {
// 默认实现:遵循者可以直接使用,也可以覆盖
func greet() -> String {
return "Hello, \(name)!"
}
// 扩展方法(不在协议要求中)
func shout() -> String {
return greet().uppercased()
}
}
struct User: Greetable {
var name: String
// 不需要实现 greet(),使用默认实现
}
let user = User(name: "Alice")
user.greet() // "Hello, Alice!"
user.shout() // "HELLO, ALICE!"
静态分发 vs 动态分发陷阱
协议要求中声明的方法通过动态分发(vtable),协议扩展中新增的方法通过静态分发:
protocol Animal {
func speak() // 协议要求 → 动态分发
}
extension Animal {
func speak() { print("...") } // 默认实现
func eat() { print("eating") } // 扩展新增 → 静态分发
}
struct Dog: Animal {
func speak() { print("Woof!") }
func eat() { print("Dog eating") }
}
let dog = Dog()
dog.speak() // "Woof!" ✅
dog.eat() // "Dog eating" ✅
let animal: Animal = Dog()
animal.speak() // "Woof!" ✅ 动态分发,调用 Dog 的实现
animal.eat() // "eating" ⚠️ 静态分发,调用扩展的默认实现!
这是 Swift 面试高频陷阱题。
协议组合
// 使用 & 组合多个协议
func processItem(_ item: Codable & Hashable) {
// item 同时遵循 Codable 和 Hashable
}
// typealias 简化
typealias SerializableItem = Codable & Hashable & Identifiable
关联类型(Associated Type)
关联类型让协议支持泛型:
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int // 通常可以省略,编译器自动推断
var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) {
items.append(item)
}
subscript(i: Int) -> Int {
return items[i]
}
}
带约束的协议扩展
// 仅当 Element 遵循 Numeric 时,才提供 sum() 方法
extension Collection where Element: Numeric {
func sum() -> Element {
reduce(0, +)
}
}
[1, 2, 3, 4].sum() // 10
[1.5, 2.5, 3.0].sum() // 7.0
// ["a", "b"].sum() // ❌ 编译错误,String 不遵循 Numeric
协议中的 init 要求
protocol Initializable {
init(value: Int)
}
// class 遵循时必须加 required
class MyClass: Initializable {
required init(value: Int) {
// ...
}
}
// 如果类是 final,可以不写 required
final class FinalClass: Initializable {
init(value: Int) { }
}
协议类型作为参数 vs 泛型约束
// 方式 1:协议类型(存在类型,existential type)
func draw(shape: any Drawable) {
shape.draw() // 动态分发,有性能开销
}
// 方式 2:泛型约束(some / 具体泛型)
func draw<T: Drawable>(shape: T) {
shape.draw() // 静态分发,编译器单态化,性能更优
}
// 方式 3:some(不透明类型,Swift 5.1+)
func makeShape() -> some Drawable {
return Circle(color: "red", radius: 10)
}
some vs any(Swift 5.7+)
some Protocol | any Protocol | |
|---|---|---|
| 分发方式 | 静态分发 | 动态分发 |
| 性能 | 更快(编译器优化) | 存在类型开销 |
| 类型信息 | 编译器知道具体类型 | 运行时擦除 |
| 使用场景 | 返回值类型固定但想隐藏 | 集合中存储不同类型 |
// some —— 返回类型固定,但调用者不知道具体是什么
func makeAnimal() -> some Animal { Dog() }
// any —— 可以存储任意遵循 Animal 的类型
var animals: [any Animal] = [Dog(), Cat()]
面向协议编程(POP)实战
用 POP 替代类继承的经典例子:
// ❌ OOP 方式:继承链容易变得复杂
class Vehicle { func start() { } }
class Car: Vehicle { override func start() { } }
class ElectricCar: Car { override func start() { } }
// ✅ POP 方式:通过协议组合能力
protocol Startable {
func start()
}
protocol Electric {
var batteryLevel: Int { get }
func charge()
}
protocol Drivable {
func drive()
}
// 自由组合,避免"上帝类"
struct Tesla: Startable, Electric, Drivable {
var batteryLevel: Int = 100
func start() { print("Silent start") }
func charge() { print("Charging...") }
func drive() { print("Driving Tesla") }
}
常见面试问题
Q1: 协议扩展中方法的动态分发和静态分发如何区分?
答案:
- 在协议要求中声明的方法 → 动态分发(通过协议见证表 Protocol Witness Table),运行时根据实际类型调用
- 仅在协议扩展中新增的方法(未在协议要求中声明) → 静态分发,编译时根据变量的声明类型决定调用
这会导致一个常见陷阱:当用协议类型引用实例时,扩展中新增的方法不会调用实际类型的重写版本。
Q2: some 和 any 关键字的区别?
答案:
some Protocol(不透明类型):编译器知道具体类型但隐藏它,使用静态分发,性能更优。适合返回值。any Protocol(存在类型):运行时擦除类型,使用动态分发有额外开销。适合存储异构集合。
Swift 5.7+ 推荐显式使用 any 标记存在类型,以区分二者。
Q3: 什么是 Protocol Witness Table?
答案:
Protocol Witness Table (PWT) 类似于 class 的 vtable,是 Swift 实现协议动态分发的机制。每个遵循协议的类型都有一张 PWT,记录了协议各方法的具体实现地址。当通过协议类型调用方法时,运行时查阅 PWT 找到正确的实现。
Q4: associatedtype 和泛型有什么区别?
答案:
- 泛型:由调用方指定类型参数,如
func sort<T: Comparable>(_ array: [T]) - associatedtype:由遵循方指定类型,如
protocol Container { associatedtype Item }
关联类型让协议更灵活,但带有 associatedtype 的协议不能直接作为类型使用(不能 var x: Container),需要通过泛型约束或 any 使用。
Q5: 为什么要面向协议编程?相比 OOP 有什么优势?
答案:
- 值类型支持:struct/enum 可以遵循协议,避免引用类型的复杂性
- 多协议遵循:无单继承限制,自由组合能力
- 默认实现:通过协议扩展提供默认行为,比抽象类更灵活
- 解耦:协议定义接口,实现可以独立变化
- 可测试性:依赖协议而非具体类型,方便 Mock
Q6: @objc protocol 和纯 Swift protocol 的区别?
答案:
| 纯 Swift Protocol | @objc Protocol | |
|---|---|---|
| 可选方法 | ❌ 不支持 | ✅ @objc optional |
| 遵循者 | struct/enum/class | 仅 class(NSObject 子类) |
| 分发 | 静态分发(扩展方法) | 动态分发(objc_msgSend) |
| 使用场景 | Swift 原生开发 | 与 UIKit/OC 互操作 |