跳到主要内容

协议与面向协议编程

问题

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 Protocolany 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: someany 关键字的区别?

答案

  • 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 有什么优势?

答案

  1. 值类型支持:struct/enum 可以遵循协议,避免引用类型的复杂性
  2. 多协议遵循:无单继承限制,自由组合能力
  3. 默认实现:通过协议扩展提供默认行为,比抽象类更灵活
  4. 解耦:协议定义接口,实现可以独立变化
  5. 可测试性:依赖协议而非具体类型,方便 Mock

Q6: @objc protocol 和纯 Swift protocol 的区别?

答案

纯 Swift Protocol@objc Protocol
可选方法❌ 不支持@objc optional
遵循者struct/enum/class仅 class(NSObject 子类)
分发静态分发(扩展方法)动态分发(objc_msgSend)
使用场景Swift 原生开发与 UIKit/OC 互操作

相关链接