协议

开篇故事

想象你开了一家餐厅。你不需要知道每个厨师来自哪里,受过什么训练,甚至不需要他们是正式员工还是临时工。你只需要确保:每个厨师都能按照菜谱做菜,都能在规定时间完成出餐,都能保证菜品卫生。

Swift 中的协议(Protocol)就是这份"菜谱"。它定义了一组要求,任何类型只要满足这些要求,就能被接受。结构体可以遵守协议,类可以遵守协议,枚举甚至可以扩展已有类型来遵守协议。

协议是 Swift 区别于其他语言的核心特性之一。Swift 倡导面向协议编程(Protocol-Oriented Programming, POP),而不是传统的类继承体系。这种设计让代码更灵活、更可组合、更容易测试。


本章适合谁

如果你理解了类、继承和值类型的概念,想理解 Swift 更灵活的抽象方式,本章适合你。如果你来自 Java 或 C#,你会发现 Swift 的协议比接口更强大。如果你来自 Rust,你会发现 Swift 协议和 Trait 有很多相似之处,但也有自己的特点。


你会学到什么

完成本章后,你可以:

  1. 使用 protocol 关键字定义协议并让类型遵守
  2. 理解协议方法、属性要求和 mutating 关键字
  3. 使用协议扩展(Protocol Extension)提供默认实现
  4. 掌握协议组合(Protocol Composition)和 some 关键字
  5. 理解面向协议编程(POP)与类继承的区别

前置要求

确保你已经阅读了 类与对象 一章,理解类、结构体、继承和 Swift 的基本类型系统。本章讨论的协议概念是在这些基础之上的更高级抽象。


第一个例子

打开 Sources/BasicSample/ExampleProtocol.swift,看最基础的定义:

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

发生了什么?

  • protocol 关键字定义协议
  • var simpleDescription: String { get } 声明一个只读属性要求
  • mutating func adjust() 声明一个会修改自身的方法要求

不同的类型可以以自己的方式遵守这个协议:

/// 结构体遵守协议
struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}

/// 类遵守协议
class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    func adjust() {  // 注意:类不需要 mutating
        simpleDescription += "  Now 100% adjusted."
    }
}

使用示例

var structure = SimpleStructure()
print(structure.simpleDescription)
structure.adjust()
print(structure.simpleDescription)

let classInstance = SimpleClass()
print(classInstance.simpleDescription)
classInstance.adjust()
print(classInstance.simpleDescription)

输出

A simple structure
A simple structure (adjusted)
A very simple class.
A very simple class.  Now 100% adjusted.

原理解析

1. 协议定义与基本遵守

协议定义类型必须满足的一组要求。一个类型可以同时遵守多个协议:

protocol Describable {
    var description: String { get }
}

protocol Identifiable {
    var id: Int { get set }
}

// 同时遵守两个协议
struct Product: Describable, Identifiable {
    let name: String
    var id: Int
    var description: String { "Product #\(id): \(name)" }
}

let product = Product(name: "Widget", id: 42)
print(product.description)  // "Product #42: Widget"

协议要求包括

  • 实例属性和类型属性(varstatic var
  • 实例方法和类型方法(funcstatic func
  • 下标(subscript
  • 协议本身也可以继承其他协议

2. mutating 关键字

mutating 是协议方法要求中的一个特殊关键字。它告诉编译器:这个方法会修改调用它的实例。

protocol Toggleable {
    var isOn: Bool { get set }
    mutating func toggle()  // 会修改自身
}

// 结构体需要 mutating
struct LightSwitch: Toggleable {
    var isOn: Bool = false
    mutating func toggle() {
        isOn = !isOn
    }
}

// 类不需要 mutating(类方法默认可以修改属性)
class Lamp: Toggleable {
    var isOn: Bool = false
    func toggle() {  // 不需要 mutating
        isOn = !isOn
    }
}

为什么需要 mutating?

因为 Swift 中结构体和枚举是值类型,默认方法不能修改自身属性。mutating 明确标记"这个方法会替换掉 self"。对于类来说,它是引用类型,方法本来就可以修改属性,所以不需要 mutating

3. 协议扩展与默认实现

协议的真正威力在于协议扩展(Protocol Extension)。你可以为协议方法提供默认实现,让遵守者可以选择不覆盖:

protocol Animal {
    func makeSound() -> String
    func greet() -> String
}

// 给 greet 提供默认实现
extension Animal {
    func greet() -> String {
        return "The animal says: \(makeSound())"
    }
}

struct Dog: Animal {
    func makeSound() -> String { "Woof" }
    // greet() 使用默认实现
}

struct Cat: Animal {
    func makeSound() -> String { "Meow" }
    // 覆盖默认实现
    func greet() -> String {
        return "Cat: \(makeSound())! *purrs*"
    }
}

print(Dog().greet())  // "The animal says: Woof"
print(Cat().greet())   // "Cat: Meow! *purrs*"

协议扩展 vs 类继承

特性类继承协议扩展
支持的类型仅类类、结构体、枚举
多继承不支持(单继承)可以实现多个协议
已有类型扩展不能扩展没有源码的类可以扩展任何类型
默认实现在父类提供在协议扩展提供
分发方式动态分发(虚函数表)静态分发(更快)

4. 协议作为类型(多态)

协议本身也是一个类型。这意味着你可以把协议用于变量声明、函数参数、数组元素等:

protocol Drawable {
    func draw() -> String
}

struct Circle: Drawable {
    func draw() -> String { "  ○  " }
}

struct Square: Drawable {
    func draw() -> String { "  ■  " }
}

// 协议作为数组元素类型
let shapes: [Drawable] = [Circle(), Square(), Circle()]

for shape in shapes {
    print(shape.draw())
}

这就是 Swift 的协议多态(Protocol Polymorphism)。它和类继承的多态效果一样,但不依赖继承体系。

来自项目中的实际应用:

// ExampleProtocol.swift 中对 Int 的扩展
extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
}

// 现在所有整数都实现了 ExampleProtocol
let x: ExampleProtocol = 7
print(x.simpleDescription)  // "The number 7"

5. associatedtype 关联类型

关联类型让协议可以定义一个"占位符类型",具体类型由遵守者决定:

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
    mutating func append(_ item: Item)
}

struct IntStack: Container {
    typealias Item = Int  // 关联类型设为 Int
    var items: [Int] = []
    var count: Int { items.count }
    subscript(i: Int) -> Int { items[i] }
    mutating func append(_ item: Int) {
        items.append(item)
    }
}

struct StringStack: Container {
    typealias Item = String  // 关联类型设为 String
    var items: [String] = []
    var count: Int { items.count }
    subscript(i: Int) -> String { items[i] }
    mutating func append(_ item: String) {
        items.append(item)
    }
}

关联类型 vs 泛型

  • 泛型参数在定义时指定(如 Array<Int>
  • 关联类型由类型的实现决定,使用者不需要显式指定

Swift 标准库中大量使用关联类型。比如 Sequence 协议就有 Element 关联类型,所以 for-in 循环能适用于 ArrayDictionaryString 等所有序列类型。

6. 协议组合

Sometimes you want a value to conform to multiple protocols simultaneously. Swift uses the & operator for protocol composition:

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

struct Person: Named, Aged {
    let name: String
    let age: Int
}

// 协议组合作为参数类型
func printInfo(_ person: Named & Aged) {
    print("\(person.name), age \(person.age)")
}

let bob = Person(name: "Bob", age: 30)
printInfo(bob)  // "Bob, age 30"

协议组合不创建新类型,它只是一个临时的"同时满足多个协议"约束。

7. some 关键字(不透明类型)

Swift 5.1 引入了 some 关键字,让函数可以返回"符合某个协议的某个具体类型",而不暴露具体类型:

protocol Shape {
    func area() -> Double
}

struct Triangle: Shape {
    let base: Double
    let height: Double
    func area() -> Double { base * height / 2 }
}

// some Shape = 返回某个遵守 Shape 的具体类型,但调用者不知道是什么
func makeDefaultShape() -> some Shape {
    Triangle(base: 10, height: 5)
}

// 调用者只能用 Shape 定义的接口
let shape = makeDefaultShape()
print(shape.area())  // 25.0
// print(shape.base)  // ❌ 编译错误!调用者不知道底层类型

some vs 协议类型的区别

// 协议类型 — 可以是任意遵守协议的类型的混合
func describe1() -> Shape {  // 返回类型可以是任何 Shape
    Triangle(base: 10, height: 5)
}
// 每次调用可以返回不同类型

// some 关键字 — 必须是某个具体类型
func describe2() -> some Shape {  // 返回类型固定,但隐藏
    Triangle(base: 10, height: 5)
}
// 每次调用必须返回相同的具体类型
// 编译器能在编译期知道具体类型,可以做更好的优化

some 关键字在 SwiftUI 中大量使用,View 协议返回值的标准写法就是 some View

8. 面向协议编程 vs 类继承

维度类继承面向协议编程(POP)
适用类型仅类类、结构体、枚举
多重继承不支持通过多协议实现
默认行为父类方法协议扩展默认实现
已有类型扩展不能可以(扩展 Int、String 等)
内存模型引用类型可选值类型或引用类型
分发动态(运行时)静态(编译时,更快)
测试性需要 mock 父类更容易 mock(值类型)

Swift 标准库几乎完全基于协议构建:EquatableComparableHashableSequenceCollectionCodable ...... 这些协议让你的自定义类型一键获得丰富的功能。


常见错误

错误 1: 结构体中忽略 mutating 关键字

protocol Counter {
    func increment()
}

struct SimpleCounter: Counter {
    var count = 0
    func increment() {  // ❌ 缺少 mutating
        count += 1
    }
}

编译器输出

error: cannot assign to property: 'self' is immutable
note: protocol requires function 'increment()' with 'mutating' modifier

修复方法

struct SimpleCounter: Counter {
    var count = 0
    mutating func increment() {  // ✅ 加 mutating
        count += 1
    }
}

错误 2: 协议作为返回类型时混用不同类型

protocol Shape {
    func draw() -> String
}

struct Circle: Shape { func draw() -> String { "○" } }
struct Square: Shape { func draw() -> String { "■" } }

func randomShape() -> Shape {
    if Bool.random() {
        return Circle()
    } else {
        return Square()   // ✅ 这可以 — 返回协议类型允许混用
    }
}

但如果用 some 关键字就不行

func randomShape() -> some Shape {  // ❌ some 要求返回具体同一类型
    if Bool.random() {
        return Circle()
    } else {
        return Square()  // 编译错误!
    }
}

编译器输出

error: function declares an opaque return type, but the return statements
       in its body do not have matching underlying types

修复方法:用协议类型(Shape)而不是不透明类型(some Shape):

func randomShape() -> Shape {  // ✅ 协议类型允许混用
    if Bool.random() {
        return Circle()
    } else {
        return Square()
    }
}

错误 3: 协议中含有 associatedtype 不能直接用作类型

protocol Container {
    associatedtype Item
    func count() -> Int
}

// ❌ 不能直接用作类型 — 编译器不知道 Item 是什么
let container: Container = ...

编译器输出

error: protocol 'Container' can only be used as a generic constraint
       because it has Self or associated type requirements

修复方法:用泛型或 some 关键字约束:

// 泛型约束
func useContainer<C: Container>(_ c: C) {
    print(c.count())
}

// 或者 some 关键字
func makeContainer() -> some Container {
    // 返回某个具体的 Container 实现
}

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
协议/接口ABC(抽象基类)/ Protocol (typing)TraitProtocolSwift 协议可以有默认实现
扩展已有类型不能(需猴子补丁)可以(impl Trait for Type)可以(extension Type: Protocol)Swift 和 Rust 都支持
多继承支持不支持(实现多个 Trait)不支持(遵守多个协议)Swift 协议组合用 &
关联类型Associated TypeassociatedtypeSwift/Rust 高度相似
不透明返回impl Traitsome Protocol等价功能
分发方式动态静态(泛型)或动态(dyn)静态(泛型/some)或动态(协议)Swift some 用静态分发
mutatingself 总是可变&mut selfmutating(值类型)概念类似,语法不同

动手练习

练习 1: 协议的基本遵守

定义一个 Mathable 协议,要求有一个 calculate() → Double 方法。让 RectangleCircle 两个结构体遵守它,各自实现面积计算。

点击查看答案
protocol Mathable {
    func calculate() -> Double
}

struct Rectangle: Mathable {
    let width: Double
    let height: Double
    func calculate() -> Double {
        width * height
    }
}

struct Circle: Mathable {
    let radius: Double
    func calculate() -> Double {
        .pi * radius * radius
    }
}

let rect = Rectangle(width: 5, height: 3)
let circle = Circle(radius: 4)
print(rect.calculate())    // 15.0
print(circle.calculate())   // 50.26548245743669

练习 2: 协议扩展默认实现

扩展上题的 Mathable 协议,添加一个 description() 方法,默认返回 "Area: X" 格式的字符串。让 Rectangle 覆盖它,返回 "Rectangle: width x height"。

点击查看答案
extension Mathable {
    func description() -> String {
        "Area: \(calculate())"
    }
}

extension Rectangle {
    func description() -> String {
        "Rectangle: \(width) x \(height)"
    }
}

let rect = Rectangle(width: 5, height: 3)
let circle = Circle(radius: 4)
print(rect.description())   // "Rectangle: 5.0 x 3.0" (覆盖实现)
print(circle.description())  // "Area: 50.26548245743669" (默认实现)

练习 3: 关联类型容器

实现一个 Stack 类型,遵守以下协议协议定义了 pushpoppeek 方法:

protocol Storable {
    associatedtype Item
    mutating func push(_ item: Item)
    mutating func pop() -> Item?
    func peek() -> Item?
}
点击查看答案
struct Stack<Item>: Storable {
    private var items: [Item] = []

    mutating func push(_ item: Item) {
        items.append(item)
    }

    mutating func pop() -> Item? {
        items.popLast()
    }

    func peek() -> Item? {
        items.last
    }
}

// 使用
var stack = Stack<Int>()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.peek() ?? "nil")  // Optional(3)
print(stack.pop() ?? "nil")   // 3
print(stack.pop() ?? "nil")   // 2

故障排查 FAQ

Q: 协议(Protocol)和类继承,我应该选哪个?

A: Swift 社区的共识是:优先考虑协议。

  • 优先用协议 + 结构体 - 值类型 + 协议组合更安全、更可测试
  • 需要共享状态或身份时 - 用类(比如 UIView 的子类化)
  • 需要扩展已有库的类型 - 只能用协议,不能继承没有源码的类
  • 多态需求 - 协议比继承更灵活,一个类型可以遵守多个协议

Apple 自己的框架也越来越多地采用协议。SwiftUI 的 View 协议就是一个协议,而不是基类。

Q: some Protocolprotocol 类型作为返回值有什么区别?

A: 关键区别在于类型确定性和性能:

  • some Protocol(不透明类型)— 编译器知道具体类型,可以做内联优化,但所有返回路径必须是同一具体类型
  • protocol(协议类型)— 运行时动态分发,允许不同返回路径,但有一点性能开销

如果你每次返回相同的类型,用 some。如果你需要返回不同类型的值,用协议类型。

Q: associatedtype 和泛型参数有什么区别?

A: 它们解决的问题不同:

  • 泛型参数 — 调用者决定。比如 Array<Int>,你告诉 Array 元素的类型
  • 关联类型 — 实现者决定。比如你实现的 Container,你把 Item 设为什么就是什么,使用者不用管

类比思考:泛型参数像是菜单上点菜(客人选),关联类型像是套餐配什么菜(厨师定)。


小结

核心要点

  1. 协议定义类型要求 - 属性、方法、下标可以成为协议要求
  2. 协议扩展提供默认实现 - 让遵守者可选覆盖,类似"继承"但不限制类型
  3. 协议作为类型使用 - 实现多态,支持 [Protocol] 数组和协议参数
  4. associatedtype 定义占位类型 - 由遵守者决定具体类型
  5. some Protocol 是不透明返回类型 - 隐藏具体类型但保留编译期类型信息

关键术语

  • Protocol: 协议(定义一组要求)
  • Conformance: 遵守(类型满足协议要求)
  • Protocol Extension: 协议扩展(为协议提供默认实现)
  • Associated Type: 关联类型(协议中的占位类型)
  • Protocol Composition: 协议组合(同时满足多个协议)
  • Opaque Type: 不透明类型(some Protocol
  • Protocol-Oriented Programming: 面向协议编程(POP)

术语表

English中文
Protocol协议
Conformance遵守
Protocol Extension协议扩展
Default Implementation默认实现
Associated Type关联类型
Protocol Composition协议组合
Opaque Type不透明类型
Protocol-Oriented Programming面向协议编程
mutating可变(方法会修改 self)
Static Dispatch静态分发
Dynamic Dispatch动态分发

完整示例:Sources/BasicSample/ExampleProtocol.swift


知识检查

问题 1 🟢 (基础概念)

protocol Greetable {
    func greet() -> String
}

struct Person: Greetable {
    let name: String
    func greet() -> String { "Hello, I'm \(name)" }
}

let p: Greetable = Person(name: "Alice")
print(p.greet())

输出是什么?

A) 编译错误 — 协议不能实例化
B) "Hello, I'm Alice"
C) "Greetable"
D) 运行时错误

答案与解析

答案: B) "Hello, I'm Alice"

解析: 协议可以作为类型使用。p 的静态类型是 Greetable,但实际存储的是 Person 实例。调用 greet() 时会动态分发给 Person 的实现。这就是协议多态。

问题 2 🟡 (mutating 关键字)

protocol Flipable {
    var state: Bool { get set }
    func flip()
}

enum Switch: Flipable {
    case on, off
    var state: Bool { self == .on }
    func flip() {
        self = (self == .on) ? .off : .on  // 修改 self
    }
}

能编译通过吗?

A) 能
B) 不能,flip 需要 mutating
C) 不能,枚举不能用 computed property
D) 不能,self 不能赋值

答案与解析

答案: B) 不能,flip 需要 mutating

解析: 枚举是值类型,修改 self(整体赋值)的方法需要标记 mutating。在协议中声明的方法也需要加 mutating

protocol Flipable {
    var state: Bool { get set }
    mutating func flip()  // ✅ 加 mutating
}

enum Switch: Flipable {
    case on, off
    var state: Bool { self == .on }
    mutating func flip() {          // ✅ 结构体/枚举需要
        self = (self == .on) ? .off : .on
    }
}

问题 3 🔴 (协议与 some 的区别)

protocol Drawable {
    func draw() -> String
}

struct Line: Drawable {
    func draw() -> String { "———" }
}

struct Box: Drawable {
    func draw() -> String { "┌─┐\n└─┘" }
}

func createDrawing(useBox: Bool) -> some Drawable {
    if useBox {
        return Box()
    } else {
        return Line()
    }
}

能编译通过吗?

A) 能
B) 不能 — some 要求所有返回路径是同一具体类型
C) 不能 — 协议需要 associatedtype
D) 能 — 会自动装箱

答案与解析

答案: B) 不能 — some 要求所有返回路径是同一具体类型

解析: some Drawable不透明类型,意味着返回值必须是某个具体的、同一的类型,只是隐藏了这个类型给调用者。if/else 返回 BoxLine 两个不同具体类型,违反了此约束。

修复 — 改用协议类型(动态分发):

func createDrawing(useBox: Bool) -> Drawable {  // ✅ 协议类型
    if useBox {
        return Box()
    } else {
        return Line()
    }
}

或者固定返回一种类型:

func createBox() -> some Drawable {   // ✅ 总是返回 Box
    Box()
}

延伸阅读

学习完协议后,你可能还想了解:

选择建议:

💡 记住:Swift 的最佳实践是"协议优先"。能不用继承就不用继承。用协议定义能力,用扩展提供默认实现,用值类型保证安全。


继续学习

  • 下一步:泛型 - 编写可复用的类型安全代码
  • 相关:类与对象 - 理解引用类型的继承体系
  • 进阶:错误处理 - Protocol + 泛型的实战应用