©️ OverlookArt
首页 / Swift / Protocols

Protocols

本文整理自 The Swift Programming Language — Protocols


概述

协议(Protocol) 定义了一套方法、属性及其他要求的蓝图,用于适配特定任务或功能。类、结构体或枚举可以采纳(adopt) 协议,并提供这些要求的具体实现。任何满足协议要求的类型都被称为遵循(conform) 该协议。

除了指定遵循类型必须实现的要求外,还可以扩展协议来实现部分要求或添加额外功能,供遵循类型使用。


协议语法

协议的定义方式与类、结构体和枚举非常相似:

1protocol SomeProtocol {
2    // 协议定义写在这里
3}

自定义类型通过在类型名称后放置协议名称(以冒号分隔)来声明采纳某个协议。可以列出多个协议,以逗号分隔:

1struct SomeStructure: FirstProtocol, AnotherProtocol {
2    // 结构体定义写在这里
3}

如果类有父类,则先列出父类,再列出协议:

1class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
2    // 类定义写在这里
3}

注意: 因为协议是类型,所以它们的名称应以大写字母开头(如 FullyNamedRandomNumberGenerator),以匹配 Swift 中其他类型的命名规范(如 IntStringDouble)。


属性要求

协议可以要求遵循类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储属性还是计算属性——它只指定所需的属性名称和类型,以及该属性必须是只读还是读写。

  • 如果协议要求属性是可读写的,则该属性不能由常量存储属性或只读计算属性来满足。
  • 如果协议只要求属性是可读的,则任何类型的属性都可以满足该要求。

属性要求始终使用 var 关键字声明为变量属性。读写属性用 { get set } 标注,只读属性用 { get } 标注:

1protocol SomeProtocol {
2    var mustBeSettable: Int { get set }
3    var doesNotNeedToBeSettable: Int { get }
4}

类型属性要求始终使用 static 关键字前缀:

1protocol AnotherProtocol {
2    static var someTypeProperty: Int { get set }
3}

示例:FullyNamed 协议

1protocol FullyNamed {
2    var fullName: String { get }
3}
1struct Person: FullyNamed {
2    var fullName: String
3}
4let john = Person(fullName: "John Appleseed")
5// john.fullName 是 "John Appleseed"

更复杂的类示例:

 1class Starship: FullyNamed {
 2    var prefix: String?
 3    var name: String
 4    init(name: String, prefix: String? = nil) {
 5        self.name = name
 6        self.prefix = prefix
 7    }
 8    var fullName: String {
 9        return (prefix != nil ? prefix! + " " : "") + name
10    }
11}
12var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
13// ncc1701.fullName 是 "USS Enterprise"

方法要求

协议可以要求遵循类型实现特定的实例方法和类型方法。这些方法写在协议定义中,与普通方法相同,但没有花括号和方法体。

类型方法要求始终以 static 关键字前缀:

1protocol SomeProtocol {
2    static func someTypeMethod()
3}

示例

1protocol RandomNumberGenerator {
2    func random() -> Double
3}
 1class LinearCongruentialGenerator: RandomNumberGenerator {
 2    var lastRandom = 42.0
 3    let m = 139968.0
 4    let a = 3877.0
 5    let c = 29573.0
 6    func random() -> Double {
 7        lastRandom = ((lastRandom * a + c)
 8            .truncatingRemainder(dividingBy: m))
 9        return lastRandom / m
10    }
11}
12let generator = LinearCongruentialGenerator()
13print("Here's a random number: \(generator.random())")
14// 打印 "Here's a random number: 0.3746499199817101"
15print("And another one: \(generator.random())")
16// 打印 "And another one: 0.729023776863283"

可变方法

值类型(结构体和枚举)的实例方法如果需要修改实例本身,需要在 func 前加 mutating 关键字。

如果协议中的实例方法要求旨在修改遵循类型的实例,则应在协议定义中标记为 mutating

1protocol Togglable {
2    mutating func toggle()
3}

注意: 如果将协议实例方法要求标记为 mutating,在类中实现该方法时不需要写 mutating 关键字。mutating 仅用于结构体和枚举。

 1enum OnOffSwitch: Togglable {
 2    case off, on
 3    mutating func toggle() {
 4        switch self {
 5        case .off:
 6            self = .on
 7        case .on:
 8            self = .off
 9        }
10    }
11}
12var lightSwitch = OnOffSwitch.off
13lightSwitch.toggle()
14// lightSwitch 现在等于 .on

初始化器

协议可以要求遵循类型实现特定的初始化器:

1protocol SomeProtocol {
2    init(someParameter: Int)
3}

类的协议初始化器实现

在遵循协议的类中实现初始化器要求时,必须使用 required 修饰符:

1class SomeClass: SomeProtocol {
2    required init(someParameter: Int) {
3        // 初始化器实现写在这里
4    }
5}

required 修饰符确保所有子类也提供该初始化器的实现,从而也遵循该协议。

如果子类重写了父类的指定初始化器,同时实现了协议的匹配初始化器要求,则需要同时标记 requiredoverride

 1protocol SomeProtocol {
 2    init()
 3}
 4
 5class SomeSuperClass {
 6    init() {
 7        // 初始化器实现
 8    }
 9}
10
11class SomeSubClass: SomeSuperClass, SomeProtocol {
12    required override init() {
13        // 初始化器实现
14    }
15}

注意: 标记为 final 的类不需要 required 修饰符,因为 final 类不能被子类化。

可失败初始化器要求

协议可以定义可失败初始化器要求(init?)。

  • 可失败初始化器要求可以由可失败或不可失败的初始化器来满足。
  • 不可失败初始化器要求只能由不可失败或隐式解包可失败初始化器来满足。

仅有语义要求的协议

并非所有协议都需要包含方法或属性要求。有些协议只描述语义要求——即关于类型行为和所支持操作的要求。

Swift 标准库定义了几个没有方法或属性要求的协议:

  • Sendable — 可以在并发域之间共享的值
  • Copyable — Swift 可以在传递给函数时复制的值
  • BitwiseCopyable — 可以逐位复制的值
1struct MyStruct: Copyable {
2    var counter = 12
3}
4
5extension MyStruct: BitwiseCopyable { }

通常不需要手动编写对这些协议的遵循——Swift 会隐式添加遵循。


协议作为类型

协议本身不实现任何功能,但可以作为类型在代码中使用。

三种使用方式

方式 说明
0 泛型约束(Generic Constraint) 适用于任何遵循协议的类型,具体类型由调用方选择
1 不透明类型(Opaque Type) API 实现选择具体类型,但对外隐藏,只保证遵循某协议
2 存在类型 / 装箱协议类型(Boxed Protocol Type) 运行时可以是任何遵循协议的类型,Swift 添加间接层(box),有性能开销

委托模式

委托是一种设计模式,允许类或结构体将部分职责交给(委托给)另一个类型的实例。

 1class DiceGame {
 2    let sides: Int
 3    let generator = LinearCongruentialGenerator()
 4    weak var delegate: Delegate?
 5
 6    init(sides: Int) {
 7        self.sides = sides
 8    }
 9
10    func roll() -> Int {
11        return Int(generator.random() * Double(sides)) + 1
12    }
13
14    func play(rounds: Int) {
15        delegate?.gameDidStart(self)
16        for round in 1...rounds {
17            let player1 = roll()
18            let player2 = roll()
19            if player1 == player2 {
20                delegate?.game(self, didEndRound: round, winner: nil)
21            } else if player1 > player2 {
22                delegate?.game(self, didEndRound: round, winner: 1)
23            } else {
24                delegate?.game(self, didEndRound: round, winner: 2)
25            }
26        }
27        delegate?.gameDidEnd(self)
28    }
29
30    protocol Delegate: AnyObject {
31        func gameDidStart(_ game: DiceGame)
32        func game(_ game: DiceGame, didEndRound round: Int, winner: Int?)
33        func gameDidEnd(_ game: DiceGame)
34    }
35}

关键点:

  • 委托属性声明为 weak 以避免强引用循环
  • 协议继承 AnyObject 标记为仅限类遵循(class-only protocol)
  • 委托属性是可选类型,使用可选链调用方法
 1class DiceGameTracker: DiceGame.Delegate {
 2    var playerScore1 = 0
 3    var playerScore2 = 0
 4
 5    func gameDidStart(_ game: DiceGame) {
 6        print("Started a new game")
 7        playerScore1 = 0
 8        playerScore2 = 0
 9    }
10
11    func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) {
12        switch winner {
13        case 1:
14            playerScore1 += 1
15            print("Player 1 won round \(round)")
16        case 2:
17            playerScore2 += 1
18            print("Player 2 won round \(round)")
19        default:
20            print("The round was a draw")
21        }
22    }
23
24    func gameDidEnd(_ game: DiceGame) {
25        if playerScore1 == playerScore2 {
26            print("The game ended in a draw.")
27        } else if playerScore1 > playerScore2 {
28            print("Player 1 won!")
29        } else {
30            print("Player 2 won!")
31        }
32    }
33}

通过扩展添加协议遵循

即使无法访问现有类型的源代码,也可以通过扩展使其采纳并遵循新协议。

1protocol TextRepresentable {
2    var textualDescription: String { get }
3}
1extension Dice: TextRepresentable {
2    var textualDescription: String {
3        return "A \(sides)-sided dice"
4    }
5}

条件遵循(Conditional Conformance)

泛型类型可能只在特定条件下才能遵循协议。使用 where 子句指定约束:

 1extension Array: TextRepresentable where Element: TextRepresentable {
 2    var textualDescription: String {
 3        let itemsAsText = self.map { $0.textualDescription }
 4        return "[" + itemsAsText.joined(separator: ", ") + "]"
 5    }
 6}
 7
 8let myDice = [d6, d12]
 9print(myDice.textualDescription)
10// 打印 "[A 6-sided dice, A 12-sided dice]"

通过扩展声明协议采纳

如果类型已经满足了协议的所有要求,但尚未声明采纳该协议,可以通过空扩展来声明:

1struct Hamster {
2    var name: String
3    var textualDescription: String {
4        return "A hamster named \(name)"
5    }
6}
7extension Hamster: TextRepresentable {}

注意: 类型不会仅因为满足协议要求就自动采纳协议,必须显式声明采纳。


合成实现(Synthesized Implementation)

Swift 可以自动为 EquatableHashableComparable 提供协议遵循,无需编写样板代码。

Equatable 的合成实现

适用于以下自定义类型:

  • 只有存储属性且都遵循 Equatable 的结构体
  • 只有关联类型且都遵循 Equatable 的枚举
  • 没有关联类型的枚举
1struct Vector3D: Equatable {
2    var x = 0.0
3    var y = 0.0
4    var z = 0.0
5}
6// == 运算符由 Swift 自动生成

Hashable 的合成实现

适用于只有遵循 Hashable 的存储属性的结构体和只有遵循 Hashable 的关联类型的枚举:

1struct GridPoint: Hashable {
2    var x: Int
3    var y: Int
4}

Comparable 的合成实现

适用于只有遵循 Comparable 的存储属性的结构体和只有遵循 Comparable 的关联类型的枚举:

1struct Version: Comparable {
2    var major: Int
3    var minor: Int
4    var patch: Int
5}
6
7let v1 = Version(major: 1, minor: 0, patch: 0)
8let v2 = Version(major: 1, minor: 1, patch: 0)
9print(v1 < v2)  // true

协议集合(Collection of Protocols)

协议可以在集合(如数组或字典)中用作类型。

1let things: [TextRepresentable] = [game, d12, simonTheHamster]
2
3for thing in things {
4    print(thing.textualDescription)
5}
6// A game of Snakes and Ladders with 25 squares
7// A 12-sided dice
8// A hamster named Simon

注意: 当协议类型用作集合中的类型时,Swift 使用存在类型(existential type),这涉及动态分发和间接层的性能开销。


协议继承

协议可以继承一个或多个其他协议,并在继承要求的基础上添加更多要求。语法类似于类继承,但可以选择列出多个父协议:

1protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
2    // 协议定义
3}

示例:

1protocol PrettyTextRepresentable: TextRepresentable {
2    var prettyTextualDescription: String { get }
3}

类专属协议(Class-Only Protocols)

通过让协议继承 AnyObject,可以限制协议只能被类类型采纳:

1protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
2    // 类专属协议定义
3}

这在委托属性需要声明为 weak 时非常有用。


协议组合(Protocol Composition)

可以同时要求类型遵循多个协议,而无需定义新的临时协议。使用 & 连接:

 1protocol Named {
 2    var name: String { get }
 3}
 4
 5protocol Aged {
 6    var age: Int { get }
 7}
 8
 9struct Person: Named, Aged {
10    var name: String
11    var age: Int
12}
13
14func wishHappyBirthday(to celebrator: Named & Aged) {
15    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
16}
17
18let birthdayPerson = Person(name: "Malcolm", age: 21)
19wishHappyBirthday(to: birthdayPerson)
20// 打印 "Happy birthday, Malcolm, you're 21!"

协议组合还可以包含一个类类型:

 1class Location {
 2    var latitude: Double
 3    var longitude: Double
 4    init(latitude: Double, longitude: Double) {
 5        self.latitude = latitude
 6        self.longitude = longitude
 7    }
 8}
 9
10class City: Location, Named {
11    var name: String
12    init(name: String, latitude: Double, longitude: Double) {
13        self.name = name
14        super.init(latitude: latitude, longitude: longitude)
15    }
16}
17
18func beginConcert(in location: Location & Named) {
19    print("Hello, \(location.name)!")
20}
21
22let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
23beginConcert(in: seattle)
24// 打印 "Hello, Seattle!"

检查协议遵循

可以使用类型转换来检查实例是否遵循某个协议:

  • is — 检查实例是否遵循某协议
  • as? — 向下转为协议类型,返回可选值
  • as! — 强制向下转为协议类型
 1protocol HasArea {
 2    var area: Double { get }
 3}
 4
 5class Circle: HasArea {
 6    let pi = 3.1415927
 7    var radius: Double
 8    var area: Double { return pi * radius * radius }
 9    init(radius: Double) { self.radius = radius }
10}
11
12class Country: HasArea {
13    var area: Double
14    init(area: Double) { self.area = area }
15}
16
17class Animal {
18    var legs: Int
19    init(legs: Int) { self.legs = legs }
20}
21
22let objects: [AnyObject] = [
23    Country(area: 243_610),
24    Circle(radius: 5.0),
25    Animal(legs: 4)
26]
27
28for object in objects {
29    if let objectWithArea = object as? HasArea {
30        print("Area is \(objectWithArea.area)")
31    } else {
32        print("Something that doesn't have an area")
33    }
34}
35// Area is 243610.0
36// Area is 78.5398175
37// Something that doesn't have an area

可选协议

可以为协议定义可选要求——遵循类型不需要实现这些要求。可选要求在协议中使用 optional 修饰符标记。

注意: 可选协议要求仅在协议标记了 @objc 属性时可用。@objc 协议只能被继承自 Objective-C 类的类或其他 @objc 类采纳,结构体和枚举不能采纳。

1@objc protocol CounterDataSource {
2    @objc optional func increment(forCount count: Int) -> Int
3    @objc optional var fixedIncrement: Int { get }
4}

调用可选方法或访问可选属性时,它们会自动变为可选类型:

 1class Counter {
 2    var count = 0
 3    var dataSource: CounterDataSource?
 4
 5    func increment() {
 6        if let amount = dataSource?.increment?(forCount: count) {
 7            count += amount
 8        } else if let amount = dataSource?.fixedIncrement {
 9            count += amount
10        }
11    }
12}

协议扩展

可以通过扩展协议来提供方法和计算属性的默认实现。遵循类型可以直接使用这些默认实现,也可以提供自己的实现来覆盖。

1extension RandomNumberGenerator {
2    func randomBool() -> Bool {
3        return random() > 0.5
4    }
5}
6
7let generator = LinearCongruentialGenerator()
8print("Here's a random Boolean: \(generator.randomBool())")
9// 打印 "Here's a random Boolean: true"

提供默认实现

可以通过协议扩展为协议的任何要求提供默认实现,包括方法、计算属性、下标和初始化器:

1extension PrettyTextRepresentable {
2    var prettyTextualDescription: String {
3        return textualDescription
4    }
5}

为协议扩展添加约束

可以通过 where 子句为协议扩展添加约束:

 1extension Collection where Element: Equatable {
 2    func allEqual() -> Bool {
 3        for element in self {
 4            if element != self.first {
 5                return false
 6            }
 7        }
 8        return true
 9    }
10}
11
12let equalDice = [d6, d6, d6]
13let differentDice = [d6, d12]
14print(equalDice.allEqual())    // true
15print(differentDice.allEqual()) // false

隐式协议遵循

Swift 会自动为某些类型添加对常见协议的遵循。例如:

  • 如果结构体或枚举的所有属性/关联类型都遵循 Equatable,则 Swift 自动为其合成 Equatable 遵循
  • 如果泛型类型的所有泛型参数都遵循某协议,Swift 可能自动添加条件遵循
  • Swift 隐式为合适的类型添加 SendableCopyable 等语义协议的遵循

总结

概念 说明
0 协议定义 protocol SomeProtocol { … }
1 属性要求 var propertyName: Type { get } 或 { get set }
2 方法要求 func methodName() -> ReturnType
3 可变方法 mutating func methodName()
4 初始化器要求 init(parameter: Type)
5 类专属协议 protocol SomeProtocol: AnyObject { … }
6 协议继承 protocol Child: Parent { … }
7 协议组合 SomeProtocol & AnotherProtocol
8 协议扩展 extension SomeProtocol { … }
9 条件遵循 extension SomeType: SomeProtocol where … { … }