Swift 5.x - 协议(中文文档)

636 阅读32分钟

引言

继续学习Swift文档,从上一章节:扩展,我们学习了Swift扩展相关的内容,主要有使用extension关键词声明扩展,扩展可以向原有类型中添加属性、实例方法、类方法、初始化方法、subscripts(下标)、定义新的嵌套类型以及使现有类符合协议等这些内容。现在,我们学习Swift协议的相关内容。由于篇幅较长,这里分篇来记录,接下来,开始吧!

协议

协议定义了适合特定任务或功能块的方法、属性和其他需求的蓝图。然后,类、结构体或枚举可以采用该协议来提供这些需求的实际实现。任何满足协议要求的类型都被称为符合该协议。

除了指定一致性类型必须实现的需求之外,您还可以扩展协议来实现这些需求中的一些,或者实现一致性类型可以利用的附加功能。

1 协议语法

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

protocol SomeProtocol {
    // protocol definition goes here
}

自定义类型通过将协议名称放在类型名称之后(用冒号分隔)作为其定义的一部分,声明它们采用特定的协议。可以列出多个协议,并用逗号分隔:

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

如果一个类有一个超类,请在它采用的任何协议之前列出超类名称,后跟逗号:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

2 属性要求

协议可以要求任何一致的类型提供具有特定名称和类型的实例属性或类型属性。协议没有指定属性是存储属性还是计算属性,它只指定所需的属性名称和类型。协议还指定每个属性必须是gettable还是gettable和settable。

如果协议要求属性是可获取和可设置的,则常量存储属性或只读计算属性将无法满足该属性要求。如果协议只要求一个属性是可获取的,那么任何类型的属性都可以满足这个要求,如果这对您自己的代码有用,那么属性也是可设置的也是有效的。

属性要求总是声明为变量属性,前缀为var关键字。Gettable和setable属性通过在类型声明之后写入{get set}来表示,而Gettable属性则通过写入{get}来表示。

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

在协议中定义类型属性要求时,始终使用static关键字作为前缀。类型属性要求在由类实现时可以以class或static关键字作为前缀,也适用于此规则:

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

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

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed协议需要一个一致的类型来提供一个完全限定的名称。协议没有规定一致性类型的任何其他性质,它只规定类型必须能够为自己提供一个全名。协议规定,任何fullyname类型都必须具有一个名为fullName的gettable实例属性,该属性的类型为String。

下面是一个采用并符合FullyNamed协议的简单结构示例:

struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

此示例定义了一个名为Person的结构体,它表示一个特定的命名人员。它声明它采用FullyNamed协议作为其定义的第一行的一部分。 Person的每个实例都有一个名为fullName的存储属性,其类型为String。这符合FullyNamed协议的单一要求,意味着Person已经正确地遵守了协议。(如果没有满足协议要求,Swift会在编译时报告错误。)

下面是一个更复杂的类,它也采用并符合FullyNamed协议:

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 is "USS Enterprise"

这个类将fullName属性需求实现为星舰的计算只读属性。每个星际飞船类实例都存储一个强制名称和一个可选前缀。fullName属性使用前缀值(如果存在的话),并将其放在名称的开头,为星际飞船创建一个完整的名称。

3 方法要求

协议可以要求特定的实例方法和类型方法由一致的类型来实现。这些方法作为协议定义的一部分进行编写,其方式与普通实例和类型方法完全相同,但没有大括号或方法体。允许使用可变参数,但要遵守与常规方法相同的规则。但是,不能为协议定义中的方法参数指定默认值。

与类型属性要求一样,在协议中定义类型方法要求时,始终使用static关键字作为其前缀。即使类型方法要求在由类实现时以class或static关键字作为前缀,也是如此:

protocol SomeProtocol {
    static func someTypeMethod()
}

例如:

protocol RandomNumberGenerator {
    func random() -> Double
}

这个协议,RandomNumberGenerator,要求任何一致的类型都有一个名为random的实例方法,该方法在调用时返回一个双精度值。虽然它没有被指定为协议的一部分,但是我们假设这个值是一个从0.0到1.0(但不包括1.0)的数字。

RandomNumberGenerator协议对如何生成每个随机数没有任何假设,它只是要求生成器提供一个标准的方法来生成一个新的随机数。

下面是一个类的实现,它采用并符合RandomNumberGenerator协议。此类实现了称为线性同余生成器的伪随机数生成器算法:

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())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"

4 可变方法要求

方法有时需要修改(或改变)它所属的实例。例如,对于值类型(即结构体和枚举)的实例方法,可以在方法的func关键字之前放置mutating关键字,以指示允许该方法修改它所属的实例和该实例的任何属性。在从实例方法内修改值类型中描述了此过程。

如果您定义了一个协议实例方法需求,该需求旨在改变采用该协议的任何类型的实例,请在协议定义中用mutating关键字标记该方法。这使得结构体和枚举能够采用协议并满足方法需求。

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

下面的示例定义了一个名为Togglable的协议,它定义了一个称为toggle的单实例方法需求。顾名思义,toggle()方法旨在切换或反转任何一致类型的状态,通常是通过修改该类型的属性。

toggle()方法被标记为mutating关键字,作为Togglable协议定义的一部分,以指示当调用该方法时,该方法将改变一致实例的状态:

protocol Togglable {
    mutating func toggle()
}

如果为结构或枚举实现可切换协议,则该结构体或枚举可以通过提供也被标记为可变的toggle()方法的实现来符合协议。

下面的示例定义了一个名为OnOffSwitch的枚举。此枚举在两种状态之间切换,由启用和禁用枚举事例指示。枚举的toggle实现标记为mutating,以匹配Togglable协议的要求:

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 is now equal to .on

4 初始化要求

协议可以要求特定的初始化器由一致的类型来实现。将这些初始值设定项作为协议定义的一部分,其编写方式与普通初始值设定项完全相同,但没有大括号或初始值设定项体:

protocol SomeProtocol {
    init(someParameter: Int)
}
4.1 协议初始值设定项需求的类实现

您可以在一致性类上实现协议初始值设定项要求,既可以是指定的初始值设定项,也可以是方便的初始值设定项。在这两种情况下,必须使用所需的修饰符标记初始值设定项实现:

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // initializer implementation goes here
    }
}

使用required修饰符可以确保在一致性类的所有子类上提供显式的或继承的初始值设定项要求实现,以便它们也符合协议。

有关所需初始值设定项的详细信息,请参阅所需初始值设定项

注意
您不需要在用final修饰符标记的类上使用所需修饰符标记协议初始值设定项实现,因为final类不能子类化。有关最终修改器的详细信息,请参见防止覆盖

如果子类重写超类中指定的初始值设定项,并且还从协议中实现匹配的初始值设定项要求,请使用required和override修饰符标记初始值设定项实现:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // initializer implementation goes here
    }
}
4.2 失败的初始值设定项要求

协议可以定义一致性类型的失败初始值设定项要求,如失败初始值设定项中定义的那样。

一个失败的初始值设定项要求可以由一致类型上的失败或不失败的初始值设定项来满足。不可用的初始值设定项要求可以由不可用的初始值设定项或隐式展开的失败初始值设定项来满足。

5 协议类型

协议本身并不实现任何功能。不过,作为一个成熟的协议,你可以在代码中使用成熟的类型。使用协议作为类型有时被称为存在类型,它来自短语“存在一个类型T使得T符合协议”。

您可以在允许其他类型的许多地方使用协议,包括:

  • 作为函数、方法或初始值设定项中的参数类型或返回类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的项的类型

注意
因为协议是类型,所以它们的名称以大写字母开头(例如FullyNamed和RandomNumberGenerator)以匹配Swift中其他类型的名称(例如Int、String和Double)。

例如:

class Dice {
    let sides: Int
    let generator: RandomNumberGenerator
    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

这个例子定义了一个名为Dice的新类,它表示在棋盘游戏中使用的n边骰子。骰子实例有一个称为sides的整数属性,表示它们有多少个边,还有一个名为generator的属性,它提供了一个随机数生成器,从中可以创建骰子掷骰值。

generator属性的类型为RandomNumberGenerator。因此,您可以将其设置为采用RandomNumberGenerator协议的任何类型的实例。除了实例必须采用RandomNumberGenerator协议外,不需要对分配给此属性的实例执行其他任何操作。因为它的类型是RandomNumberGenerator,Dice类中的代码只能以应用于所有符合此协议的生成器的方式与生成器交互。这意味着它不能使用由生成器的基础类型定义的任何方法或属性。但是,您可以从协议类型向下转换到底层类型,方法与从父类向下转换到子类的方式相同,如向下转换中所述。

Dice还有一个初始值设定项,用于设置其初始状态。这个初始值设定项有一个名为generator的参数,它也是RandomNumberGenerator类型。在初始化一个新的Dice实例时,可以将任何一致类型的值传递给此参数。

Dice提供了一个实例方法roll,它返回一个介于1和骰子边数之间的整数值。此方法调用生成器的random()方法创建一个介于0.0和1.0之间的新随机数,并使用此随机数在正确的范围内创建骰子掷骰值。因为generator采用RandomNumberGenerator,所以保证有一个random()方法来调用。

下面是Dice类如何使用linearcongreentialGenerator实例作为随机数生成器创建一个六边骰子:

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4

6 代理

委托是一种设计模式,它使类或结构能够将其部分职责移交(或委托)给另一种类型的实例。这个设计模式是通过定义一个协议来实现的,该协议封装了被委托的职责,这样一个一致的类型(称为委托)被保证提供已经被委托的功能。委派可用于响应特定操作,或从外部源检索数据,而无需知道该源的基础类型。 下面的示例定义了两种用于基于骰子的棋盘游戏的协议:

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

骰子游戏协议是任何涉及骰子的游戏都可以采用的协议。 DiceGameDelegate协议可用于跟踪骰子游戏的进度。为了防止强引用循环,委托被声明为弱引用。有关弱引用的信息,请参见类实例之间的强引用循环。将协议标记为类只允许本章后面的SnakesAndLadders类声明其委托必须使用弱引用。纯类协议通过从任何对象继承来标记,如纯类协议中所述。 这是一个最初在控制流中引入的蛇和梯子游戏的版本。此版本适用于将骰子实例用于骰子掷骰子;采用骰子游戏协议;并通知骰子游戏代表其进度:

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    init() {
        board = Array(repeating: 0, count: finalSquare + 1)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    weak var delegate: DiceGameDelegate?
    func play() {
        square = 0
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
            switch square + diceRoll {
            case finalSquare:
                break gameLoop
            case let newSquare where newSquare > finalSquare:
                continue gameLoop
            default:
                square += diceRoll
                square += board[square]
            }
        }
        delegate?.gameDidEnd(self)
    }
}

有关蛇和梯子游戏的描述,请参见Break。

这个版本的游戏被包装成一个名为SnakesAndLadders的类,它采用DiceGame协议。它提供了一个gettable dice属性和play()方法,以符合协议。(dice属性声明为常量属性,因为初始化后不需要更改,协议只要求它必须是可获取的。)

Snakes and Ladders游戏板设置在类的init()初始值设定项中进行。所有游戏逻辑都被移到协议的play方法中,该方法使用协议所需的dice属性来提供掷骰子的值。

请注意,delegate属性被定义为可选的DiceGameDelegate,因为玩游戏不需要委托。因为它是可选类型,委托属性会自动设置为初始值nil。此后,游戏实例化器可以选择将属性设置为合适的委托。因为DiceGameDelegate协议是类的,所以可以声明委托是弱的,以防止引用循环。

DiceGameDelegate提供了三种跟踪游戏进度的方法。这三个方法已经被合并到上面play()方法中的游戏逻辑中,并且在新游戏开始、新回合开始或游戏结束时调用。

因为delegate属性是可选的DiceGameDelegate,play()方法每次调用委托上的方法时都使用可选的链接。如果delegate属性为nil,则这些委托调用将正常失败且不会出错。如果delegate属性为非nil,则调用委托方法,并将其作为参数传递给SnakesAndLadders实例。

下一个示例显示了一个名为DiceGameTracker的类,它采用DiceGameDelegate协议:

class DiceGameTracker: DiceGameDelegate {
    var numberOfTurns = 0
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        if game is SnakesAndLadders {
            print("Started a new game of Snakes and Ladders")
        }
        print("The game is using a \(game.dice.sides)-sided dice")
    }
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}

DiceGameTracker实现了DiceGameDelegate所需的所有三种方法。它使用这些方法来跟踪一个游戏的回合数。当游戏开始时,它将numberOfTurns属性重置为零,每次新回合开始时递增,并在游戏结束后打印出总回合数。

上面所示的gameDidStart(:)的实现使用game参数打印有关即将玩的游戏的一些介绍性信息。game参数的类型是DiceGame,而不是SnakesAndLadders,因此gameDidStart(:)只能访问和使用作为DiceGame协议一部分实现的方法和属性。但是,该方法仍然能够使用类型转换来查询基础实例的类型。在这个例子中,它检查游戏是否实际上是幕后的蛇和梯子的实例,如果是,则打印一条适当的消息。

gameDidStart(:)方法还访问传递的游戏参数的dice属性。因为已知game符合DiceGame协议,所以它保证有一个dice属性,因此gameDidStart(:)方法可以访问和打印骰子的sides属性,而不管玩的是哪种游戏。

以下是DiceGameTracker在实际操作中的外观:

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Started a new game of Snakes and Ladders
// The game is using a 6-sided dice
// Rolled a 3
// Rolled a 5
// Rolled a 4
// Rolled a 5
// The game lasted for 4 turns

7 使用扩展添加协议一致性

您可以扩展现有类型以采用并遵守新的协议,即使您无权访问现有类型的源代码。扩展可以向现有类型添加新的属性、方法和下标,因此能够添加协议可能需要的任何需求。有关扩展的详细信息,请参见扩展

注意
当一个类型的现有实例在扩展中添加到实例的类型时,该类型的现有实例将自动采用并遵守协议。

例如,这个被称为TextRepresentable的协议,可以由任何可以用文本表示的类型来实现。这可能是对其自身的描述,也可能是其当前状态的文本版本:

protocol TextRepresentable {
    var textualDescription: String { get }
}

上面的Dice类可以扩展为采用并符合TextRepresentable:

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

这个扩展采用新协议的方式与Dice在其原始实现中提供的方式完全相同。协议名在类型名之后提供,用冒号分隔,协议的所有要求的实现都在扩展的大括号内提供。

任何骰子实例现在都可以被视为TextRepresentable:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"

类似地,SnakesAndLadders游戏类可以扩展为采用并符合TextRepresentable协议:

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)
// Prints "A game of Snakes and Ladders with 25 squares"
7.1 有条件地遵守协议

泛型类型只能在某些条件下满足协议的要求,例如当类型的泛型参数符合协议时。通过在扩展泛型类型时列出约束,可以使泛型类型有条件地符合协议。通过编写一个通用的where子句,将这些约束写在所采用的协议的名称之后。有关泛型where子句的更多信息,请参见泛型where子句

以下扩展使数组实例无论何时存储符合TextRepresentable类型的元素都符合TextRepresentable协议。

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)
// Prints "[A 6-sided dice, A 12-sided dice]"
7.2 用扩展声明协议

如果某个类型已经符合某个协议的所有要求,但尚未声明它采用该协议,则可以使其采用具有空扩展名的协议:

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

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

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"

注意
类型不会仅仅通过满足协议的需求就自动采用协议。它们必须始终明确使用该协议。

8 使用自动合成实现协议

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

Swift为以下类型的自定义类型提供了equalable的自动合成实现:

  • 结构体,这些结构只具有符合equalable协议的存储属性
  • 仅具有符合Equatable协议的关联类型的枚举
  • 没有关联类型的枚举

要接收==的自动合成实现,请在包含原始声明的文件中声明对Equatable的一致性,而不需要自己实现==运算符。Equatable协议提供了的默认实现!=.

下面的例子定义了三维位置向量(x,y,z)的Vector3D结构,类似于Vector2D结构。因为x、y和z属性都是一个等价类型,Vector3D接收等价运算符的自动合成实现。

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.")
}
// Prints "These two vectors are also equivalent."

Swift为以下类型的自定义类型提供了Hashable的自动合成实现:

  • 只具有符合哈希协议的存储属性的结构体
  • 只有符合哈希协议的关联类型的枚举
  • 没有关联类型的枚举

要接收hash(into:)的合成实现,请在包含原始声明的文件中声明与Hashable的一致性,而不自己实现hash(into:)方法。

Swift为没有原始值的枚举提供了Comparable的综合实现。如果枚举具有关联的类型,则它们必须全部符合可比较协议。若要接收<的合成实现,请在包含原始枚举声明的文件中声明与Comparable的一致性,而无需自己实现<运算符。可比较协议的默认实现<=,>,和>=提供其余的比较运算符。

下面的示例为初学者、中级和专家定义了一个SkillLevel枚举。此外,专家还根据他们拥有的明星数量进行排名。

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)
}
// Prints "beginner"
// Prints "intermediate"
// Prints "expert(stars: 3)"
// Prints "expert(stars: 5)"

9 协议类型集合

协议可以用作存储在数组或字典等集合中的类型,如协议中提到的类型。这个例子创建了一个文本表示的数组:

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

现在可以迭代数组中的项,并打印每个项的文本描述:

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每次都是循环是安全的。

10 协议继承

协议可以继承一个或多个其他协议,并且可以在继承的需求之上添加更多的需求。协议继承的语法与类继承的语法类似,但可以选择列出多个继承的协议,用逗号分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}

下面是继承TextRepresentable协议的例子:

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

这个例子定义了一个新协议PrettyTextRepresentable,它继承了TextRepresentable。采用PrettyTextRepresentable的任何东西都必须满足TextRepresentable实施的所有要求,以及由PrettyTextRepresentable实施的附加要求。在本例中,PrettyTextRepresentable添加了一个要求,以提供一个名为prettyTextualDescription的可获取属性,该属性返回一个字符串。

蛇形梯类可扩展为采用并符合PrettyTextRepresentable:

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属性现在可用于打印任何蛇和梯子实例的漂亮文本说明:

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

11 Class-Only协议

通过将AnyObject协议添加到协议的继承列表中,可以将协议采用限制为类类型(而不是结构体或枚举)。

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // class-only protocol definition goes here
}

在上面的示例中,SomeClassOnlyProtocol只能由类类型采用。编写试图采用某个ClassOnlyProtocol的结构或枚举定义是一个编译时错误。

注意
当协议需求定义的行为假设或要求一致性类型具有引用语义而不是值语义时,使用Class-Only协议。有关引用和值语义的更多信息,请参见结构体和枚举是值类型类是引用类型

12 协议组成

要求一个类型同时符合多个协议是很有用的。您可以通过协议组合将多个协议组合成单个需求。协议组合的行为就像您定义了一个临时本地协议,该协议具有组合中所有协议的组合需求。协议组合不定义任何新的协议类型。

协议的组成有SomeProtocol和AnotherProtocol的形式。您可以根据需要列出任意多个协议,用和号(&)分隔它们。除了协议列表之外,协议组合还可以包含一个类类型,您可以使用它来指定所需的超类。

下面是一个示例,它将名为Named和Aged的两个协议组合成一个函数参数的协议组合要求:

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)
// Prints "Happy birthday, Malcolm, you're 21!"

在本例中,命名协议对名为name的可获取字符串属性有一个单独的要求。Aged协议对名为age的可获取Int属性有一个单独的要求。这两种协议都由一个叫做Person的结构体所采用。

该示例还定义了一个wishHappyBirthday(to:)函数。celebrator参数的类型命名为Named & Aged,这意味着“符合Named协议和Aged协议的任何类型”。传递给函数的特定类型无关紧要,只要它同时符合两个必需的协议。

然后,该示例创建一个名为birthdayPerson的新Person实例,并将该新实例传递给wishHappyBirthday(to:)函数。因为Person符合这两个协议,所以这个调用是有效的,wishHappyBirthday(to:)函数可以打印它的生日贺词。

下面是一个将上一个示例中的命名协议与Location类相结合的示例:

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)
// Prints "Hello, Seattle!"

begincert(in:)函数接受Location& Named类型的参数,这意味着“作为Location的子类且符合命名协议的任何类型”。在本例中,City满足这两个要求。

将birthdayPerson传递给BeginCert(in:)函数无效,因为Person不是Location的子类。同样,如果您创建了一个不符合命名协议的Location子类,那么使用该类型的实例调用beginocert(in:)也是无效的。

13 检查协议一致性

您可以使用类型转换中描述的is和as运算符检查协议一致性,并强制转换为特定协议。检查协议并将其转换为协议与检查类型并将其转换为类型所遵循的语法完全相同:

  • 如果实例符合协议,则is运算符返回true;如果实例不符合协议,则返回false。
  • as?downcast运算符返回协议类型的可选值,如果实例不符合该协议,则该值为nil。
  • as!downcast运算符的版本强制将向下转换为协议类型,并在向下转换失败时触发运行时错误。

此示例定义了一个名为HasArea的协议,其中一个属性要求一个名为area的可获取双属性:

protocol HasArea {
    var area: Double { get }
}

这里有两个类,Circle和Country,它们都符合HasArea协议:

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类基于存储的radius属性将area属性需求实现为计算属性。Country类直接将面积要求作为存储属性实现。两个类都正确地符合HasArea协议。

这里有一个名为Animal的类,它不符合HasArea协议:

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

Circle、Country和Animal类没有共享的基类。尽管如此,它们都是类,因此这三种类型的实例都可以用来初始化存储AnyObject类型值的数组:

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

objects数组初始化为一个数组文本,其中包含一个半径为2个单位的Circle实例;一个以英国的表面积(平方公里)初始化的Country实例;一个有四条腿的Animal实例。

现在可以迭代objects数组,检查数组中的每个对象是否符合HasArea协议:

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属性。

14 可选协议

您可以定义协议的可选要求。这些需求不必由符合协议的类型来实现。作为协议定义的一部分,可选要求以optional修饰符作为前缀。可选需求可用,因此您可以编写与Objective-C交互操作的代码。协议和可选需求都必须标记为@objc属性。注意@objc协议只能由继承自Objective-C类或其他@objc类的类采用。它们不能被结构体或枚举所采用。

在可选需求中使用方法或属性时,其类型自动变为可选。例如,(Int)->String类型的方法变成((Int) -> String)?。注意,整个函数类型包装在可选的,而不是方法的返回值中。

可选协议需求可以使用可选链接调用,以说明需求没有由符合协议的类型实现的可能性。当方法被调用时,通过在方法名称后面写一个问号来检查可选方法的实现,例如someOptionalMethod?(someArgument)。有关可选链接的信息,请参见可选链接

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

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

CounterDataSource协议定义了一个称为increment(forCount:)的可选方法需求和名为fixedIncrement的可选属性需求。这些需求为数据源定义了两种不同的方法,以便为计数器实例提供适当的增量。

注意
严格地说,您可以编写一个符合CounterDataSource的自定义类,而无需实现任何一个协议要求。毕竟,它们都是可选的。虽然在技术上是允许的,但这并不是一个很好的数据源。

下面定义的Counter类具有类型为CounterDataSource?的可选数据源属性:

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的变量属性中。increments属性也被称为“每一个时间”的方法。

increment()方法首先尝试通过在其数据源上查找increment(forCount:)方法的实现来检索增量。increment()方法使用可选的链接来尝试调用increment(forCount:),并将当前的count值作为方法的单个参数传递。

请注意,这里有两个可选链接级别。首先,dataSource可能是nil,因此dataSource的名称后面有一个问号,表示只有在dataSource不是nil时才应该调用increment(forCount:)。其次,即使dataSource确实存在,也不能保证它实现increment(forCount:),因为这是一个可选的要求。这里,increment(forCount:)可能无法实现的可能性也通过可选的链接来处理。对increment(forCount:)的调用仅在increment(forCount:)存在时发生,也就是说,如果increment不是nil。这就是为什么increment(forCount:)的名称后面也写有问号。

因为对increment(forCount:)的调用可能由于以下两个原因之一而失败,因此调用返回一个可选的Int值。即使increment(forCount:)定义为返回CounterDataSource定义中的非可选Int值,也是如此。即使有两个可选的链接操作,一个接一个,结果仍然被包装在一个可选的。有关使用多个可选链接操作的详细信息,请参见链接多个级别的链接

在调用increment(forCount:)之后,它返回的可选Int将使用可选绑定展开为一个名为amount的常量。如果可选Int包含一个值,也就是说,如果委托和方法都存在,并且该方法返回了一个值,则展开的量将被添加到存储的count属性中,并完成递增。

如果无法从increment(forCount:)方法中检索值,或者因为数据源为空,或者因为数据源没有实现increment(forCount:)—那么increment()方法将尝试从数据源的fixedIncrement属性中检索值。fixedIncrement属性也是可选要求,因此它的值是可选Int值,即使fixedIncrement作为CounterDataSource协议定义的一部分被定义为非可选Int属性。

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

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

您可以使用ThreeSource的实例作为新计数器实例的数据源:

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

上面的代码创建一个新的Counter实例;将其数据源设置为新的ThreeSource实例;并调用Counter的increment()方法四次。正如预期的那样,每次调用increment()时,计数器的count属性将增加3。 下面是一个更复杂的数据源TowardsZeroSource,它使计数器实例从其当前的计数值向上或向下计数为零:

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计数到零。一旦计数器达到零,则不再进行计数:

counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0

15 协议扩展

协议可以被扩展,以提供方法、初始值设定项、下标和计算属性实现以符合要求的类型。这允许您在协议本身上定义行为,而不是在每个类型的单独一致性或全局函数中定义行为。

例如,可以扩展RandomNumberGenerator协议以提供randomBool()方法,该方法使用所需random()方法的结果返回随机Bool值:

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

通过在协议上创建一个扩展,所有符合条件的类型都自动获得该方法的实现,而无需任何额外的修改。

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// Prints "And here's a random Boolean: true"

协议扩展可以向一致性类型添加实现,但不能使协议扩展或从另一个协议继承。协议继承总是在协议声明本身中指定。

15.1 提供默认实现

可以使用协议扩展为该协议的任何方法或计算属性需求提供默认实现。如果一致性类型提供了所需方法或属性的自身实现,则将使用该实现而不是扩展提供的实现。

注意
扩展提供的默认实现的协议要求与可选协议要求不同。尽管一致性类型不必提供它们自己的实现,但是具有默认实现的需求可以在没有可选链接的情况下被调用。

例如,继承TextRepresentable协议的PrettyTextRepresentable协议可以提供其所需prettyTextualDescription属性的默认实现,以简单地返回访问textualDescription属性的结果:

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}
15.2 向协议扩展添加约束

定义协议扩展时,可以指定一致性类型在扩展的方法和属性可用之前必须满足的约束。通过编写一个通用的where子句,可以将这些约束写在要扩展的协议的名称之后。有关泛型where子句的更多信息,请参见泛型where子句

例如,可以定义集合协议的扩展,该扩展应用于元素符合equalable协议的任何集合。你可以用一个标准集合的一部分来约束它!=用于检查两个元素之间是否相等的运算符。

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}

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

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

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

因为数组符合集合,整数符合equalable,所以equalNumbers和differentNumbers可以使用allEqual()方法:

print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

注意
如果一致性类型满足为同一方法或属性提供实现的多个约束扩展的要求,Swift将使用与最特殊的约束相对应的实现。

参考文档:Swift - Protocols