2.23 协议

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/protocols/

2.23 协议

定义遵循类型必须实现的要求。

协议为适合某项任务或某段功能的方法、属性及其他要求定义了一份蓝图。随后类、结构体或枚举可以采纳该协议,为这些要求提供实际实现。任何满足协议要求的类型都称为遵循该协议。

除了规定遵循类型必须实现的要求之外,你还可以扩展协议来实现其中的部分要求,或者实现遵循类型可以利用的额外功能。

协议语法

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

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

自定义类型在定义时把协议名写在类型名之后、用冒号分隔,以此声明它采纳某个协议。可以列出多个协议,用逗号分隔:

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

如果类有父类,就把父类名写在任何它采纳的协议之前,后跟一个逗号:

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

注意:由于协议是类型, 它们的名字应以大写字母开头 (例如 FullyNamed 和 RandomNumberGenerator), 与 Swift 中其他类型的名字 (例如 Int、String 和 Double)保持一致。

属性要求

协议可以要求任何遵循类型提供具有特定名字和类型的实例属性或类型属性。协议不规定该属性应当是存储属性还是计算属性——它只规定所需的属性名和类型;协议还规定每个属性必须是可获取的,还是既可获取又可设置的。

如果协议要求某个属性既可获取又可设置,那么该属性要求不能由常量存储属性或只读计算属性满足。如果协议只要求属性可获取,那么任何种类的属性都可以满足该要求;如果对你自己的代码有用,该属性同时可设置也是允许的。

属性要求总是声明为变量属性,以 var 关键字开头。可获取且可设置的属性在类型声明之后写 { get set } 表示,可获取的属性写 { get } 表示。

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

在协议中定义类型属性要求时,始终用 static 关键字作前缀。尽管类在实现这些类型属性要求时可以用 class 或 static 关键字作前缀,这条规则依然成立:

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

下面是一个带单条实例属性要求的协议示例:

1
2
3
protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed 协议要求遵循类型提供一个完整的名字。协议没有对遵循类型的性质做任何其他规定——它只规定该类型必须能为自己提供一个完整名字。协议声明任何 FullyNamed 类型都必须有一个名为 fullName、类型为 String 的可获取实例属性。

下面是一个采纳并遵循 FullyNamed 协议的简单结构体示例:

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

这个例子定义了一个名为 Person 的结构体,表示某个有名有姓的人;它在定义的第一行声明采纳 FullyNamed 协议。

每个 Person 实例都有一个名为 fullName、类型为 String 的存储属性,这与 FullyNamed 协议的唯一要求相符,意味着 Person 正确地遵循了该协议。(如果协议要求没有被满足,Swift 会在编译期报错。)

下面是一个更复杂的类,它也采纳并遵循 FullyNamed 协议:

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

这个类把 fullName 属性要求实现为一个只读计算属性,用于表示一艘星舰。每个 Starship 类实例都存放一个必需的 name 和一个可选的 prefix;fullName 属性在 prefix 存在时使用它,并把它前置到 name 之前,从而形成星舰的完整名字。

方法要求

协议可以要求遵循类型实现特定的实例方法和类型方法。这些方法写在协议定义中,写法与普通实例方法和类型方法完全相同,但不写花括号,也不写方法体。允许使用可变参数,规则与普通方法相同;不过,协议定义中的方法参数不能指定默认值。

与类型属性要求一样,在协议中定义类型方法要求时始终用 static 关键字作前缀。尽管类在实现这些类型方法要求时可以用 class 或 static 关键字作前缀,这一点依然成立:

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

下面的例子定义了一个带单条实例方法要求的协议:

1
2
3
protocol RandomNumberGenerator {
    func random() -> Double
}

这个 RandomNumberGenerator 协议要求任何遵循类型都有一个名为 random 的实例方法,每次调用返回一个 Double 值。虽然协议没有明确规定,但可以假定这个值是 0.0 到(不含)1.0 之间的数字。

RandomNumberGenerator 协议不对每个随机数如何生成做任何假设——它只要求生成器提供一种生成新随机数的标准方式。

下面是一个采纳并遵循 RandomNumberGenerator 协议的类实现,它实现了一种称为线性同余生成器的伪随机数算法:

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

可变方法要求

有时方法需要修改(变更)它所属的实例。对于值类型(即结构体和枚举)的实例方法,你在方法的 func 关键字之前写 mutating 关键字,表示该方法可以修改它所属的实例以及该实例的属性。这一过程在在实例方法中修改值类型中介绍。

如果你定义的协议实例方法要求意在修改任何采纳该协议的类型的实例,就在协议定义中给该方法标记 mutating 关键字。这让结构体和枚举可以采纳该协议并满足该方法要求。

注意:如果你把协议的实例方法要求标记为 mutating, 在为类编写该方法的实现时就不需要写 mutating 关键字。 mutating 关键字只由结构体和枚举使用。

下面的例子定义了一个名为 Togglable 的协议,它定义了一条名为 toggle 的实例方法要求。顾名思义,toggle() 方法意在切换或反转任何遵循类型的状态,通常是通过修改该类型的某个属性来实现。

toggle() 方法在 Togglable 协议定义中被标记了 mutating 关键字,表示该方法在调用时会修改遵循实例的状态:

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

如果你为结构体或枚举实现 Togglable 协议,该结构体或枚举可以通过提供一个同样标记为 mutating 的 toggle() 方法实现来遵循该协议。

下面的例子定义了一个名为 OnOffSwitch 的枚举,它在由枚举成员 on 和 off 表示的两种状态之间切换。这个枚举的 toggle 实现被标记为 mutating,以符合 Togglable 协议的要求:

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

构造器要求

协议可以要求遵循类型实现特定的构造器。这些构造器写在协议定义中,写法与普通构造器完全相同,但不写花括号,也不写构造器体:

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

类对协议构造器要求的实现

在遵循的类上,你可以把协议的构造器要求实现为指定构造器或便利构造器。两种情况下都必须用 required 修饰符标记该构造器实现:

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

使用 required 修饰符可确保你在遵循类的所有子类上都提供该构造器要求的显式或继承实现,使它们也遵循该协议。

关于必需构造器的更多内容,参见必需构造器。

注意:在标记了 final 修饰符的类上, 你不需要用 required 修饰符标记协议构造器的实现, 因为 final 类不能被继承。 关于 final 修饰符的更多内容,参见阻止覆盖。

如果子类覆盖了父类的某个指定构造器,同时又实现了协议中相匹配的构造器要求,就把该构造器实现同时标记 required 和 override 修饰符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // 构造器实现写在这里
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" 来自对 SomeProtocol 的遵循;"override" 来自 SomeSuperClass
    required override init() {
        // 构造器实现写在这里
    }
}

可失败构造器要求

协议可以为遵循类型定义可失败构造器要求,定义方式见可失败构造器。

可失败构造器要求可以由遵循类型上的可失败构造器或非可失败构造器满足;非可失败构造器要求可以由非可失败构造器或隐式解包的可失败构造器满足。

只有语义要求的协议

上面所有示例协议都要求某些方法或属性,但协议声明并不一定要包含任何要求。你也可以用协议来描述语义要求——也就是关于这些类型的值如何表现、以及它们支持哪些操作的要求。

Swift 标准库定义了若干不含任何必需方法或属性的协议:

关于这些协议要求的更多内容,参见它们文档中的概述。

采纳这些协议使用的语法与采纳其他协议相同,唯一区别是不需要包含实现协议要求的方法或属性声明。例如:

1
2
3
4
5
struct MyStruct: Copyable {
    var counter = 12
}

extension MyStruct: BitwiseCopyable { }

上面的代码定义了一个新结构体。由于 Copyable 只有语义要求,结构体声明中没有为采纳该协议写任何代码;同样,由于 BitwiseCopyable 只有语义要求,采纳该协议的扩展体是空的。

通常你不需要写出对这些协议的遵循性——Swift 会隐式替你添加,详见对协议的隐式遵循。

协议作为类型

协议本身并不实现任何功能。尽管如此,你仍然可以在代码中把协议当作类型使用。

把协议当作类型使用的最常见方式,是把协议用作泛型约束。带泛型约束的代码可以处理任何遵循该协议的类型,具体类型由使用该 API 的代码选择。例如,当你调用一个接受实参、而该实参类型是泛型的函数时,类型由调用方选择。

使用不透明类型的代码处理遵循该协议的某种类型:底层类型在编译期已知,由 API 实现选择该类型,但该类型的身份对 API 的调用方隐藏。使用不透明类型可以防止 API 的实现细节透过抽象层泄漏——例如隐藏函数的具体返回类型,只保证该值遵循给定协议。

使用装箱协议类型的代码处理遵循该协议、在运行时选定的任何类型。为了支持这种运行时灵活性,Swift 在必要时会增加一层间接引用——称为装箱,它会带来性能开销。由于这种灵活性,Swift 在编译期并不知道底层类型,这意味着你只能访问协议所要求的成员;要访问底层类型上的其他 API,需要在运行时做转换。

关于把协议用作泛型约束的内容,参见泛型;关于不透明类型和装箱协议类型的内容,参见不透明类型与装箱协议类型。

委托

委托是一种设计模式,让类或结构体把自己的一部分职责移交(或委托)给另一个类型的实例。实现这种设计模式的方式,是定义一个封装被委托职责的协议,从而保证遵循类型(称为委托对象)提供被委托的功能。委托可以用来响应某个特定动作,或者从外部来源取数据,而不必知道该来源的底层类型。

下面的例子定义了一个骰子游戏,以及一个用于跟踪游戏进度的委托的嵌套协议:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class DiceGame {
    let sides: Int
    let generator = LinearCongruentialGenerator()
    weak var delegate: Delegate?

    init(sides: Int) {
        self.sides = sides
    }

    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }

    func play(rounds: Int) {
        delegate?.gameDidStart(self)
        for round in 1...rounds {
            let player1 = roll()
            let player2 = roll()
            if player1 == player2 {
                delegate?.game(self, didEndRound: round, winner: nil)
            } else if player1 > player2 {
                delegate?.game(self, didEndRound: round, winner: 1)
            } else {
                delegate?.game(self, didEndRound: round, winner: 2)
            }
        }
        delegate?.gameDidEnd(self)
    }

    protocol Delegate: AnyObject {
        func gameDidStart(_ game: DiceGame)
        func game(_ game: DiceGame, didEndRound round: Int, winner: Int?)
        func gameDidEnd(_ game: DiceGame)
    }
}

DiceGame 类实现的游戏由玩家轮流掷骰子,点数最高的玩家赢得该回合。它使用本章前面例子中的线性同余生成器为掷骰子生成随机数。

DiceGame.Delegate 协议可以被采纳用于跟踪骰子游戏的进度。由于 DiceGame.Delegate 协议总是在骰子游戏的上下文中使用,它被嵌套在 DiceGame 类内部。只要外层声明不是泛型的,协议就可以嵌套在结构体和类之类的类型声明内部。关于嵌套类型的内容,参见嵌套类型。

为了防止强引用循环,委托被声明为弱引用。关于弱引用的内容,参见类实例之间的强引用循环。把协议标记为类专用协议,可以让 DiceGame 类声明它的委托必须使用弱引用。类专用协议通过继承 AnyObject 来标记,详见类专用协议。

DiceGame.Delegate 提供三个方法用于跟踪游戏进度,这三个方法被整合进上面 play(rounds:) 方法的游戏逻辑中:DiceGame 类在新游戏开始、新回合开始或游戏结束时调用自己的委托方法。

由于 delegate 属性是可选的 DiceGame.Delegate,play(rounds:) 方法每次调用委托上的方法时都使用可选链,详见可选链。如果 delegate 属性是 nil,这些委托调用会被忽略;如果 delegate 属性不是 nil,就会调用委托方法,并把 DiceGame 实例作为参数传入。

下一个例子展示了一个名为 DiceGameTracker 的类,它采纳了 DiceGame.Delegate 协议:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class DiceGameTracker: DiceGame.Delegate {
    var playerScore1 = 0
    var playerScore2 = 0
    func gameDidStart(_ game: DiceGame) {
        print("Started a new game")
        playerScore1 = 0
        playerScore2 = 0
    }
    func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) {
        switch winner {
            case 1:
                playerScore1 += 1
                print("Player 1 won round \(round)")
            case 2: playerScore2 += 1
                print("Player 2 won round \(round)")
            default:
                print("The round was a draw")
        }
    }
    func gameDidEnd(_ game: DiceGame) {
        if playerScore1 == playerScore2 {
            print("The game ended in a draw.")
        } else if playerScore1 > playerScore2 {
            print("Player 1 won!")
        } else {
            print("Player 2 won!")
        }
    }
}

DiceGameTracker 类实现了 DiceGame.Delegate 协议要求的全部三个方法:它用这些方法在新游戏开始时把两名玩家的分数清零,在每个回合结束时更新分数,并在游戏结束时宣布获胜者。

DiceGame 与 DiceGameTracker 的实际用法如下:

1
2
3
4
5
6
7
8
9
let tracker = DiceGameTracker()
let game = DiceGame(sides: 6)
game.delegate = tracker
game.play(rounds: 3)
// Started a new game
// Player 2 won round 1
// Player 2 won round 2
// Player 1 won round 3
// Player 2 won!

用扩展添加协议遵循性

你可以扩展一个已有类型,让它采纳并遵循一个新协议,即使你拿不到该已有类型的源代码。扩展可以给已有类型添加新属性、新方法和新下标,因此能够添加协议可能要求的任何要求。关于扩展的更多内容,参见扩展。

注意:当某个类型在扩展中加上对某个协议的遵循性时, 该类型已有的实例会自动采纳并遵循这个协议。

例如,下面这个名为 TextRepresentable 的协议,可以被任何能以文本表示自己的类型实现——这种表示可以是它自身的描述,也可以是它当前状态的文本版本:

1
2
3
protocol TextRepresentable {
    var textualDescription: String { get }
}

可以扩展上面的 Dice 类,让它采纳并遵循 TextRepresentable:

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

这个扩展采纳新协议的方式,与 Dice 在原始实现中提供该协议时完全相同:协议名写在类型名之后、用冒号分隔,并在扩展的花括号中提供协议全部要求的实现。

现在任何 Dice 实例都可以当作 TextRepresentable 处理:

1
2
3
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// 输出 "A 12-sided dice"。

类似地,可以扩展 SnakesAndLadders 游戏类,让它采纳并遵循 TextRepresentable 协议:

1
2
3
4
5
6
7
extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)
// 输出 "A game of Snakes and Ladders with 25 squares"。

有条件地遵循协议

泛型类型可能只在特定条件下才能满足某个协议的要求,例如当该类型的泛型形参遵循该协议时。你可以在扩展该类型时列出约束,让泛型类型有条件地遵循某个协议:在所要采纳的协议名之后,用泛型 where 子句写出这些约束。关于泛型 where 子句的更多内容,参见泛型 where 子句。

下面的扩展让 Array 实例在存放的元素类型遵循 TextRepresentable 时遵循 TextRepresentable 协议。

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

用扩展声明协议采纳

如果某个类型已经满足某个协议的全部要求,但还没有声明采纳该协议,你可以用一个空扩展让它采纳:

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

现在只要需要 TextRepresentable 类型,就可以使用 Hamster 的实例:

1
2
3
4
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 输出 "A hamster named Simon"。

注意:仅仅满足协议要求并不会让类型自动采纳该协议, 类型必须始终显式声明对协议的采纳。

用合成实现采纳协议

在许多简单情况下,Swift 可以自动为 Equatable、Hashable 和 Comparable 提供协议遵循性。使用这种合成实现,你就不必自己编写重复的样板代码来实现协议要求。

对于下列自定义类型,Swift 提供 Equatable 的合成实现:

  • 只含遵循 Equatable 协议之存储属性的结构体
  • 只含遵循 Equatable 协议之关联类型的枚举
  • 没有关联类型的枚举

要获得 == 的合成实现,请在包含原始声明的文件中声明遵循 Equatable,而不必自己实现 == 运算符。Equatable 协议提供了 != 的默认实现。

下面的例子定义了一个 Vector3D 结构体,表示三维位置向量 (x, y, z),与 Vector2D 结构体类似。由于 x、y、z 属性都是 Equatable 类型,Vector3D 会获得等价运算符的合成实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Vector3D: Equatable {
    var x = 0.0, y = 0.0, z = 0.0
}

let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
if twoThreeFour == anotherTwoThreeFour {
    print("These two vectors are also equivalent.")
}
// 输出 "These two vectors are also equivalent."。

对于下列自定义类型,Swift 提供 Hashable 的合成实现:

  • 只含遵循 Hashable 协议之存储属性的结构体
  • 只含遵循 Hashable 协议之关联类型的枚举
  • 没有关联类型的枚举

要获得 hash(into:) 的合成实现,请在包含原始声明的文件中声明遵循 Hashable,而不必自己实现 hash(into:) 方法。

对于没有原始值的枚举,Swift 提供 Comparable 的合成实现;如果该枚举有关联类型,它们都必须遵循 Comparable 协议。要获得 < 的合成实现,请在包含原始枚举声明的文件中声明遵循 Comparable,而不必自己实现 < 运算符。Comparable 协议对 <=、> 和 >= 的默认实现提供了其余比较运算符。

下面的例子定义了一个 SkillLevel 枚举,包含初学者、中级和专家三种成员,其中专家还按星数排名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum SkillLevel: Comparable {
    case beginner
    case intermediate
    case expert(stars: Int)
}
var levels = [SkillLevel.intermediate, SkillLevel.beginner,
              SkillLevel.expert(stars: 5), SkillLevel.expert(stars: 3)]
for level in levels.sorted() {
    print(level)
}
// 输出 "beginner"。
// 输出 "intermediate"。
// 输出 "expert(stars: 3)"。
// 输出 "expert(stars: 5)"。

对协议的隐式遵循

有些协议太常见,以至于你几乎每次声明新类型时都会写上它们。对于下列协议,当你定义的类型实现了协议要求时,Swift 会自动推断出遵循性,因此你不必自己写:

你仍然可以显式写出遵循性,但这不会改变代码的行为。要抑制隐式遵循性,就在遵循性列表中的协议名前写一个波浪号(~):

1
2
3
struct FileDescriptor: ~Sendable {
    let rawValue: Int
}

上面的代码展示了围绕 POSIX 文件描述符的包装类型的一部分。FileDescriptor 结构体满足 Sendable 协议的全部要求,通常这会让它可发送;然而写 ~Sendable 会抑制这一隐式遵循性。虽然文件描述符用整数来标识和操作打开的文件、而整数值本身是可发送的,但让它不可发送有助于避免某些类型的 bug。

抑制隐式遵循性的另一种方式是用一个标记为不可用的扩展:

1
2
@available(*, unavailable)
extension FileDescriptor: Sendable { }

当你在代码的某一处写 ~Sendable(如上一个例子)时,程序其他地方仍然可以扩展 FileDescriptor 类型来添加 Sendable 遵循性。相比之下,这个例子中的不可用扩展既抑制了对 Sendable 的隐式遵循性,也阻止了代码中其他任何扩展为该类型添加 Sendable 遵循性。

注意: 除了上面讨论的协议之外, 分布式 actor 还隐式遵循 Codable 协议。

协议类型的集合

如把协议当作类型中所述,协议可以用作数组或字典之类集合中存放的元素类型。下面这个例子创建了一个包含 TextRepresentable 元素的数组:

1
let things: [TextRepresentable] = [game, d12, simonTheHamster]

现在就可以遍历数组中的元素,打印每个元素的文本描述:

1
2
3
4
5
6
for thing in things {
    print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

注意常量 thing 的类型是 TextRepresentable,而不是 Dice、DiceGame 或 Hamster,即使背后实际的实例是其中某种类型。不过,由于它的类型是 TextRepresentable,而任何 TextRepresentable 都已知具有 textualDescription 属性,因此每轮循环中访问 thing.textualDescription 都是安全的。

协议继承

协议可以继承一个或多个其他协议,并在所继承要求的基础上添加更多要求。协议继承的语法与类继承类似,区别是可以列出多个被继承的协议,用逗号分隔:

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

下面是一个继承上面 TextRepresentable 协议的示例:

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

这个例子定义了一个新协议 PrettyTextRepresentable,它继承自 TextRepresentable。任何采纳 PrettyTextRepresentable 的类型都必须满足 TextRepresentable 强制的全部要求,外加 PrettyTextRepresentable 强制的额外要求。在这个例子里,PrettyTextRepresentable 添加了一条要求:提供一个名为 prettyTextualDescription、类型为 String 的可获取属性。

可以扩展 SnakesAndLadders 类,让它采纳并遵循 PrettyTextRepresentable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\n"
        for index in 1...finalSquare {
            switch board[index] {
            case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}

这个扩展声明采纳 PrettyTextRepresentable 协议,并为 SnakesAndLadders 类型提供 prettyTextualDescription 属性的实现。任何 PrettyTextRepresentable 也必然是 TextRepresentable,因此 prettyTextualDescription 的实现先访问 TextRepresentable 协议中的 textualDescription 属性作为输出字符串的开头:它追加一个冒号和一个换行,作为漂亮文本表示的起始;随后遍历棋盘格数组,为每格的内容追加一个几何图形:

  • 如果该格的值大于 0,它就是梯子底部,用 ▲ 表示。
  • 如果该格的值小于 0,它就是蛇头,用 ▼ 表示。
  • 否则该格的值为 0,是"空格",用 ○ 表示。

现在就可以用 prettyTextualDescription 属性打印任何 SnakesAndLadders 实例的漂亮文本描述:

1
2
3
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

类专用协议

你可以通过在协议的继承列表中加入 AnyObject 协议,把协议采纳限制为类类型(而不允许结构体或枚举):

1
2
3
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // 类专用协议的定义写在这里
}

在上面的例子里,SomeClassOnlyProtocol 只能被类类型采纳。编写试图采纳 SomeClassOnlyProtocol 的结构体或枚举定义会导致编译期错误。

注意:当协议要求所定义的行为假定或要求 遵循类型具有引用语义而非值语义时, 请使用类专用协议。 关于引用语义和值语义的更多内容, 参见结构体与枚举是值类型 和类是引用类型。

协议组合

要求一个类型同时遵循多个协议可能很有用。你可以用协议组合把多个协议合并成单条要求。协议组合的行为就像你定义了一个临时的局部协议,它具有组合中所有协议要求的并集;协议组合不会定义任何新的协议类型。

协议组合的形式是 SomeProtocol & AnotherProtocol。你可以按需列出任意多个协议,用和号(&)分隔。除了协议列表,协议组合还可以包含一个类类型,用来指定必需的父类。

下面是一个把 Named 和 Aged 两个协议组合成函数参数上单条协议组合要求的例子:

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

在这个例子里,Named 协议只有一条要求:名为 name 的可获取 String 属性;Aged 协议只有一条要求:名为 age 的可获取 Int 属性。这两个协议都被名为 Person 的结构体采纳。

这个例子还定义了 wishHappyBirthday(to:) 函数,其中 celebrator 参数的类型是 Named & Aged,意思是"任何同时遵循 Named 和 Aged 协议的类型"。传给函数的究竟是哪个具体类型并不重要,只要它同时遵循这两个要求的协议即可。

接着这个例子创建一个名为 birthdayPerson 的新 Person 实例,并把它传给 wishHappyBirthday(to:) 函数。由于 Person 同时遵循两个协议,这次调用是合法的,wishHappyBirthday(to:) 函数可以打印出它的生日祝福。

下面是一个把上一个例子中的 Named 协议与 Location 类组合起来的例子:

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

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// 输出 "Hello, Seattle!"。

beginConcert(in:) 函数接受的参数类型是 Location & Named,意思是"任何 Location 的子类、且遵循 Named 协议的类型"。在这个例子里,City 同时满足这两项要求。

把 birthdayPerson 传给 beginConcert(in:) 函数是不合法的,因为 Person 不是 Location 的子类;同样,如果你写一个不遵循 Named 协议的 Location 子类,用该类型的实例调用 beginConcert(in:) 也不合法。

检查协议遵循性

你可以用类型转换中介绍的 is 和 as 运算符检查协议遵循性,并转换到特定协议。检查和转换到协议的语法与检查和转换到类型完全相同:

  • 如果实例遵循某个协议,is 运算符返回 true,否则返回 false。
  • as? 形式的向下转换运算符返回协议类型的可选值;如果实例不遵循该协议,该值为 nil。
  • as! 形式的向下转换运算符强制向下转换到协议类型,如果转换不成功就触发运行时错误。

下面这个例子定义了一个名为 HasArea 的协议,只有一条要求:名为 area 的可获取 Double 属性:

1
2
3
protocol HasArea {
    var area: Double { get }
}

下面是两个类 Circle 和 Country,它们都遵循 HasArea 协议:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

Circle 类把 area 属性要求实现为基于存储属性 radius 的计算属性;Country 类则直接把 area 要求实现为存储属性。两个类都正确地遵循了 HasArea 协议。

下面是一个名为 Animal 的类,它不遵循 HasArea 协议:

1
2
3
4
class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

Circle、Country 和 Animal 三个类没有共同的父类;尽管如此,它们都是类,因此这三种类型的实例都可以用来初始化一个存放 AnyObject 类型值的数组:

1
2
3
4
5
let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

objects 数组用一个数组字面量初始化,其中包含半径为 2 个单位的 Circle 实例、用英国国土面积(平方千米)初始化的 Country 实例,以及四条腿的 Animal 实例。

现在就可以遍历 objects 数组,检查其中每个对象是否遵循 HasArea 协议:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

只要数组中的某个对象遵循 HasArea 协议,as? 运算符返回的可选值就会被可选绑定解包到名为 objectWithArea 的常量中。常量 objectWithArea 的类型已知是 HasArea,因此可以以类型安全的方式访问并打印它的 area 属性。

注意转换过程不会改变底层对象:它们仍然是 Circle、Country 和 Animal。不过当它们被存放在常量 objectWithArea 中时,已知的类型只有 HasArea,因此只能访问它们的 area 属性。

可选协议要求

你可以为协议定义可选要求:遵循该协议的类型不必实现这些要求。可选要求在协议定义中以 optional 修饰符开头。提供可选要求是为了让代码能与 Objective-C 互操作——协议和可选要求都必须标记 @objc 特性。注意 @objc 协议只能被类采纳,不能被结构体或枚举采纳。

当你使用可选要求中的方法或属性时,它的类型会自动变为可选类型。例如类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。注意这里被包进可选类型的是整个函数类型,而不是方法的返回值。

可选协议要求可以用可选链调用,以应对遵循该协议的类型没有实现该要求的情况。调用可选方法时,在方法名之后写一个问号,即可检查它是否已实现,例如 someOptionalMethod?(someArgument)。关于可选链的内容,参见可选链。

下面的例子定义了一个名为 Counter 的整数计数类,它用一个外部数据源提供递增量。这个数据源由 CounterDataSource 协议定义,该协议有两个可选要求:

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

CounterDataSource 协议定义了一条名为 increment(forCount:) 的可选方法要求和一条名为 fixedIncrement 的可选属性要求,它们定义了数据源为 Counter 实例提供合适递增量的两种不同方式。

注意:严格来说,你可以写一个既没有实现 CounterDataSource 任何一条协议要求的自定义类 来遵循该协议——毕竟两条要求都是可选的。 虽然技术上允许,但这样的数据源并不好用。

下面定义的 Counter 类有一个类型为 CounterDataSource? 的可选 dataSource 属性:

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

Counter 类用一个名为 count 的变量属性存放当前值,还定义了一个名为 increment 的方法,每次调用都给 count 属性加一。

increment() 方法首先尝试从数据源上查找 increment(forCount:) 方法的实现来取得递增量:它用可选链尝试调用 increment(forCount:),并把当前的 count 值作为该方法唯一的实参传入。

注意这里两层可选链同时在起作用:第一,dataSource 可能是 nil,因此 dataSource 的名字后面有问号,表示只有当 dataSource 不是 nil 时才调用 increment(forCount:);第二,即使 dataSource 存在,也不能保证它实现了 increment(forCount:),因为这是可选要求——这里同样用可选链处理这种可能:只有当 increment(forCount:) 存在(也就是不为 nil)时才会调用它。这正是 increment(forCount:) 的名字后面也写问号的原因。

由于这次调用可能因为上述两个原因之一而失败,它返回的是可选 Int 值——即使 CounterDataSource 定义中把 increment(forCount:) 声明为返回非可选 Int 也依然如此。尽管这里接连发生了两次可选链操作,结果仍然只被包在一层可选值中。关于使用多层可选链的更多内容,参见串联多层链。

调用 increment(forCount:) 之后,它返回的可选 Int 通过可选绑定被解包到常量 amount 中。如果这个可选 Int 确实包含值——也就是委托和方法都存在、并且方法返回了值——就把解包后的 amount 加到存储属性 count 上,递增完成。

如果无法从 increment(forCount:) 方法取得值——或者因为 dataSource 是 nil,或者因为数据源没有实现 increment(forCount:)——那么 increment() 方法就改为尝试从数据源的 fixedIncrement 属性取值。fixedIncrement 同样是可选要求,因此它的值是可选 Int 值,即使 CounterDataSource 协议定义中把 fixedIncrement 声明为非可选 Int 属性也是如此。

下面是一个简单的 CounterDataSource 实现:数据源每次被查询都返回值 3,它通过实现可选的 fixedIncrement 属性要求做到这一点:

1
2
3
class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

你可以把 ThreeSource 的实例用作新 Counter 实例的数据源:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

上面的代码创建一个新的 Counter 实例,把它的数据源设为新的 ThreeSource 实例,并调用计数器的 increment() 方法四次。正如预期,每次调用 increment() 后计数器的 count 属性都增加三。

下面是一个更复杂的数据源 TowardsZeroSource,它让 Counter 实例从当前的 count 值向零递增或递减:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

TowardsZeroSource 类实现了 CounterDataSource 协议中可选的 increment(forCount:) 方法,并用 count 实参的值判断应当朝哪个方向计数。如果 count 已经是零,该方法返回 0,表示不应再继续计数。

你可以把 TowardsZeroSource 的实例与已有的 Counter 实例配合,从 -4 数到零;计数器到达零之后就不再计数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0

协议扩展

协议可以被扩展,从而为遵循类型提供方法、构造器、下标和计算属性的实现。这让你能在协议本身上定义行为,而不必写在每个类型各自的遵循实现里,也不必写在全局函数中。

例如,可以扩展 RandomNumberGenerator 协议来提供 randomBool() 方法,它利用必需的 random() 方法的结果返回一个随机 Bool 值:

1
2
3
4
5
extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

通过在协议上创建扩展,所有遵循类型都会自动获得这个方法的实现,无需任何额外修改。

1
2
3
4
5
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 输出 "Here's a random number: 0.3746499199817101"。
print("And here's a random Boolean: \(generator.randomBool())")
// 输出 "And here's a random Boolean: true"。

协议扩展可以为遵循类型添加实现,但不能让协议扩展或继承另一个协议;协议继承始终写在协议声明本身之中。

提供默认实现

你可以用协议扩展为该协议的任何方法或计算属性要求提供默认实现。如果遵循类型为某个必需方法或属性提供了自己的实现,就会使用该实现,而不是扩展提供的那个。

注意:带扩展所提供默认实现的协议要求, 与可选的协议要求是两回事。 虽然遵循类型对两者都不必提供自己的实现, 但带默认实现的要求可以不用可选链直接调用。

例如,继承了 TextRepresentable 协议的 PrettyTextRepresentable 协议,可以为其必需的 prettyTextualDescription 属性提供默认实现,直接返回访问 textualDescription 属性的结果:

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

为协议扩展添加约束

定义协议扩展时,你可以指定遵循类型必须满足的约束,扩展的方法和属性才会可用。在所要扩展的协议名之后写一个泛型 where 子句来书写这些约束。关于泛型 where 子句的更多内容,参见泛型 where 子句。

例如,你可以定义一个 Collection 协议的扩展,让它适用于元素遵循 Equatable 协议的任何集合。把集合元素约束为遵循 Swift 标准库中的 Equatable 协议后,你就能用 == 和 != 运算符检查两个元素是否相等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}

只有当集合中所有元素都相等时,allEqual() 方法才返回 true。

考虑两个整数数组,一个所有元素都相同,另一个则不然:

1
2
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]

由于数组遵循 Collection、整数遵循 Equatable,equalNumbers 和 differentNumbers 都可以使用 allEqual() 方法:

1
2
3
4
print(equalNumbers.allEqual())
// 输出 "true"。
print(differentNumbers.allEqual())
// 输出 "false"。

注意:如果遵循类型同时满足多个带约束扩展的要求, 而这些扩展为同一个方法或属性提供了实现, Swift 会使用约束最具体的那一个实现。