为有牺牲多壮志,敢教日月换新天。

Swift5.4 语言指南(二十三) 协议

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(shanqingyongzhi)
➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址:https://www.cnblogs.com/strengthen/p/9739783.html 
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

热烈欢迎,请直接点击!!!

进入博主App Store主页,下载使用各个作品!!!

注:博主将坚持每月上线一个新app!!!

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

除了指定必须符合标准的类型的要求之外,您还可以扩展协议以实现这些要求中的某些要求,或者实施符合标准的类型可以利用的其他功能。

协议语法

您以与类,结构和枚举非常相似的方式定义协议:

  1. protocol SomeProtocol {
  2. // protocol definition goes here
  3. }

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

  1. struct SomeStructure: FirstProtocol, AnotherProtocol {
  2. // structure definition goes here
  3. }

如果一个类具有超类,请在其采用的任何协议之前列出超类名称,并以逗号开头:

  1. class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
  2. // class definition goes here
  3. }

物业要求

协议可以要求任何符合条件的类型来提供具有特定名称和类型的实例属性或类型属性。协议没有指定该属性是存储属性还是计算属性,仅指定所需的属性名称和类型。该协议还指定每个属性必须是可获取的还是获取的可设置的。

如果协议要求某个属性必须是可获取和可设置的,则该属性的要求不能由常量存储的属性或只读的计算属性来满足。如果协议仅要求一个属性是可获取的,则该要求可以由任何种类的属性来满足,并且对于可用于您自己的代码的属性,也可以对其进行设置是有效的。

属性要求始终声明为变量属性,并以var关键字为前缀gettable和settable属性在类型声明后通过写来指示,而gettable属性通过write来指示get set }get }

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

static在协议中定义类型属性要求时,请始终在其前面加上关键字。即使类型属性要求在由类实现时可以以classorstatic关键字作为前缀,该规则也适用

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

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

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

FullyNamed协议需要一个符合标准的类型来提供完全限定的名称。该协议未指定有关符合类型的性质的任何其他信息,仅指定该类型必须能够为其自身提供全名。该协议规定,任何FullyNamed类型必须有称为gettable实例属性fullName,它的类型的String

这是采用并符合FullyNamed协议的简单结构的示例

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

本示例定义了一个名为的结构Person,该结构代表一个特定的命名人。它声明它采用该FullyNamed协议作为其定义的第一行的一部分。

的每个实例Person都有一个名为的存储属性fullName,类型为String这符合FullyNamed协议的单一要求,并且意味着Person已正确符合协议。(如果未满足协议要求,Swift会在编译时报告错误。)

这是一个更复杂的类,它也采用并遵守该FullyNamed协议:

  1. class 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. }
  12. var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
  13. // ncc1701.fullName is "USS Enterprise"

此类将fullName属性要求实现为飞船的已计算的只读属性。每个Starship类实例存储一个必填name和一个可选的prefixfullName属性使用该prefix值(如果存在),并将其添加到的开头name以为星际飞船创建全名。

方法要求

协议可能要求特定的实例方法和类型方法要通过符合类型来实现。这些方法以与普通实例和类型方法完全相同的方式编写为协议定义的一部分,但没有花括号或方法主体。可变参数是允许的,但要遵循与常规方法相同的规则。但是,不能在协议的定义中为方法参数指定默认值。

与类型属性要求一样,static在协议中定义类型方法要求,请始终在其前面加上关键字。即使类型方法要求在由类实现时classorstatic关键字为前缀也是如此

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

以下示例定义了一个具有单实例方法要求的协议:

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

该协议RandomNumberGenerator要求任何符合条件的类型都有一个称为的实例方法random,该实例方法在被调用时会返回一个Double值。尽管未将其指定为协议的一部分,但假设此值是从0.0的数字(但不包括)1.0

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

这是采用并符合RandomNumberGenerator协议的类的实现此类实现称为线性同余生成器的伪随机数生成器算法

  1. class 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. }
  12. let generator = LinearCongruentialGenerator()
  13. print("Here's a random number: \(generator.random())")
  14. // Prints "Here's a random number: 0.3746499199817101"
  15. print("And another one: \(generator.random())")
  16. // Prints "And another one: 0.729023776863283"

变异方法要求

有时有必要使用一种方法来修改(或变异)它所属的实例。对于值类型(即结构和枚举)的实例方法,请将mutating关键字放在方法的func关键字之前,以指示允许该方法修改其所属的实例以及该实例的任何属性。在实例方法修改值类型中介绍了此过程

如果您定义了一个协议实例方法要求,该要求旨在使采用该协议的任何类型的实例发生变异,请将该方法标记为mutating关键字,作为协议定义的一部分。这使结构和枚举可以采用该协议并满足该方法要求。

笔记

如果将协议实例方法的要求标记为mutating,则mutating在为类编写该方法的实现时不需要编写关键字。mutating关键字仅由结构和枚举。

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

协议定义中,toggle()mutating关键字标记方法Togglable,以表明该方法在被调用时将使符合实例的状态发生变化:

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

如果您Togglable为某个结构或枚举实现协议,则该结构或枚举可以通过提供该toggle()方法的实现(也标记为)来符合该协议mutating

以下示例定义了一个名为的枚举OnOffSwitch此枚举在两个状态之间切换,由枚举用on和表示off枚举的toggle实现标记为mutating,以符合Togglable协议的要求:

  1. enum 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. }
  12. var lightSwitch = OnOffSwitch.off
  13. lightSwitch.toggle()
  14. // lightSwitch is now equal to .on

初始化程序要求

协议可能要求特定的初始化程序通过一致的类型来实现。您可以使用与普通初始化程序完全相同的方式将这些初始化程序编写为协议定义的一部分,但不使用花括号或初始化程序主体:

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

协议初始化程序要求的类实现

您可以在符合条件的类上实现协议初始化程序要求,既可以将其指定为初始化程序,也可以作为便捷初始化程序。在这两种情况下,都必须使用required修饰符标记初始化程序的实现

  1. class SomeClass: SomeProtocol {
  2. required init(someParameter: Int) {
  3. // initializer implementation goes here
  4. }
  5. }

使用required修饰符可确保您在符合类的所有子类上提供初始化程序要求的显式或继承实现,以使它们也符合协议。

有关必需的初始化程序的更多信息,请参见必需的初始化程序

笔记

您不需要在用required修饰符标记的类上修饰符标记协议初始化程序实现final,因为最终类不能被子类化。有关final修饰符的更多信息,请参见防止覆盖

如果子类覆盖超类中的指定初始化程序,并且还通过协议实现了匹配的初始化程序要求,请同时使用requiredoverride修饰符标记初始化程序的实现

  1. protocol SomeProtocol {
  2. init()
  3. }
  4. class SomeSuperClass {
  5. init() {
  6. // initializer implementation goes here
  7. }
  8. }
  9. class SomeSubClass: SomeSuperClass, SomeProtocol {
  10. // "required" from SomeProtocol conformance; "override" from SomeSuperClass
  11. required override init() {
  12. // initializer implementation goes here
  13. }
  14. }

初始化器失败要求

协议可以为一致性类型定义失败的初始化器要求,如Failable Initializers中所定义

合格的初始化器要求可以由合格或不合格的初始化器类型满足。不可失败的初始化器或隐式展开的可失败初始化器可以满足不可失败的初始化器要求。

协议作为类型

协议本身实际上并没有实现任何功能。但是,您可以将协议用作代码中的完整类型。将协议用作类型有时有时称为存在类型,它来自短语“存在类型T,使得T符合协议”。

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

  • 作为函数,方法或初始化程序中的参数类型或返回类型
  • 作为常量,变量或属性的类型
  • 作为数组,字典或其他容器中项目的类型

笔记

由于协议的类型,开始他们的名称以大写字母(如FullyNamedRandomNumberGenerator),以配合其他类型的雨燕的名称(如IntStringDouble)。

这是用作类型的协议的示例:

  1. class Dice {
  2. let sides: Int
  3. let generator: RandomNumberGenerator
  4. init(sides: Int, generator: RandomNumberGenerator) {
  5. self.sides = sides
  6. self.generator = generator
  7. }
  8. func roll() -> Int {
  9. return Int(generator.random() * Double(sides)) + 1
  10. }
  11. }

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

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

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

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

Dice是使用类创建LinearCongruentialGenerator随机实例生成器的六面骰子的方法

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

代表团

委托是一种设计模式,使类或结构可以其某些职责移交给(或委托)另一种类型的实例。通过定义封装委托职责的协议来实现此设计模式,从而确保符合类型(称为委托)可以提供已委托的功能。委托可用于响应特定操作,或从外部源检索数据而无需了解该源的基础类型。

下面的示例定义了两种用于基于骰子的棋盘游戏的协议:

  1. protocol DiceGame {
  2. var dice: Dice { get }
  3. func play()
  4. }
  5. protocol DiceGameDelegate: AnyObject {
  6. func gameDidStart(_ game: DiceGame)
  7. func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
  8. func gameDidEnd(_ game: DiceGame)
  9. }

DiceGame协议是可以被涉及骰子的任何游戏采用的协议。

DiceGameDelegate协议可以用来跟踪进度DiceGame为防止强引用循环,将委托声明为弱引用。有关弱引用的信息,请参见类实例之间的强引用循环将协议标记为仅类,可以使SnakesAndLadders本章稍后类声明其委托必须使用弱引用。只有A类的协议是通过从它的继承标记AnyObject,如在讨论类只有协议

这是最初在Control Flow中引入Snakes and Ladders游戏的一个版本该版本适用于其骰子实例;采纳该协议;并通知其进度:DiceDiceGameDiceGameDelegate

  1. class SnakesAndLadders: DiceGame {
  2. let finalSquare = 25
  3. let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
  4. var square = 0
  5. var board: [Int]
  6. init() {
  7. board = Array(repeating: 0, count: finalSquare + 1)
  8. board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
  9. board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
  10. }
  11. weak var delegate: DiceGameDelegate?
  12. func play() {
  13. square = 0
  14. delegate?.gameDidStart(self)
  15. gameLoop: while square != finalSquare {
  16. let diceRoll = dice.roll()
  17. delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
  18. switch square + diceRoll {
  19. case finalSquare:
  20. break gameLoop
  21. case let newSquare where newSquare > finalSquare:
  22. continue gameLoop
  23. default:
  24. square += diceRoll
  25. square += board[square]
  26. }
  27. }
  28. delegate?.gameDidEnd(self)
  29. }
  30. }

有关“蛇和梯子”游戏玩法的描述,请参见Break

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

蛇和梯子游戏板的设置采取类的内进行init()初始化。所有游戏逻辑都移到了协议的play方法中,该方法使用协议的requireddice属性提供骰子掷骰值。

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

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

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

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

  1. class DiceGameTracker: DiceGameDelegate {
  2. var numberOfTurns = 0
  3. func gameDidStart(_ game: DiceGame) {
  4. numberOfTurns = 0
  5. if game is SnakesAndLadders {
  6. print("Started a new game of Snakes and Ladders")
  7. }
  8. print("The game is using a \(game.dice.sides)-sided dice")
  9. }
  10. func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
  11. numberOfTurns += 1
  12. print("Rolled a \(diceRoll)")
  13. }
  14. func gameDidEnd(_ game: DiceGame) {
  15. print("The game lasted for \(numberOfTurns) turns")
  16. }
  17. }

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

gameDidStart(_:)上面显示的实现使用该game参数来打印有关将要玩的游戏的一些介绍性信息。game参数具有类型的DiceGame,而不是SnakesAndLaddersgameDidStart(_:)可以访问和使用只方法和被实现为的部件属性DiceGame的协议。但是,该方法仍然可以使用类型转换来查询基础实例的类型。在此示例中,它检查game实际上是否SnakesAndLadders幕后实例,如果,则打印适当的消息。

gameDidStart(_:)方法还访问dice传递的game参数属性由于game已知符合该DiceGame协议,因此可以保证具有dice属性,因此该gameDidStart(_:)方法能够访问和打印骰子的sides属性,而不管正在玩哪种游戏。

这是实际的DiceGameTracker外观:

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

通过扩展添加协议一致性

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

笔记

当在扩展中将该一致性添加到实例的类型时,该类型的现有实例会自动采用并符合协议。

例如,此协议称为TextRepresentable,可以通过任何一种可以表示为文本的方式来实现。这可能是对自身的描述,也可能是其当前状态的文本版本:

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

Dice上面类可以扩展为采用和遵循TextRepresentable

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

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

Dice现在可以将任何实例视为TextRepresentable

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

同样,SnakesAndLadders可以将游戏类扩展为采用并符合TextRepresentable协议:

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

有条件地遵守协议

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

以下扩展使Array实例TextRepresentable在它们存储符合的类型的元素时就符合协议TextRepresentable

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

声明协议采用扩展

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

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

Hamster现在可以TextRepresentable在所需类型的任何地方使用的实例

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

笔记

类型不会仅通过满足协议的要求就自动采用协议。他们必须始终明确声明其对协议的采用。

通过综合实现采用协议

斯威夫特可以自动提供协议一致性的EquatableHashable以及Comparable在很多简单的情况。使用这种综合的实现意味着您不必编写重复的样板代码即可自己实现协议要求。

Swift提供了Equatable以下几种自定义类型的综合实现

  • 仅存储符合Equatable协议属性的结构
  • 仅具有符合Equatable协议的关联类型的枚举
  • 没有关联类型的枚举

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

以下示例Vector3D为三维位置矢量定义了一种结构,类似于该结构。因为性能都是一个的类型,接收合成的等价运营商的实现。(x, y, z)Vector2DxyzEquatableVector3D

  1. struct Vector3D: Equatable {
  2. var x = 0.0, y = 0.0, z = 0.0
  3. }
  4. let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
  5. let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
  6. if twoThreeFour == anotherTwoThreeFour {
  7. print("These two vectors are also equivalent.")
  8. }
  9. // Prints "These two vectors are also equivalent."

Swift提供了Hashable以下几种自定义类型的综合实现

  • 仅存储符合Hashable协议属性的结构
  • 仅具有符合Hashable协议的关联类型的枚举
  • 没有关联类型的枚举

要接收的综合实现hash(into:),请Hashable在包含原始声明的文件中声明对的符合性,而无需hash(into:)自己实现方法。

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

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

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

协议类型的集合

协议可用作要存储在诸如数组或字典之类的集合中的类型,如“协议作为类型”中所述这个例子创建了一个TextRepresentable东西数组

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

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

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

请注意,thing常数为类型TextRepresentable它不是类型Dice,或DiceGameHamster,即使幕后的实际实例是这些类型之一。尽管如此,由于它的类型为TextRepresentable,并且任何TextRepresentable已知的都具有textualDescription属性,因此thing.textualDescription每次通过循环访问都是安全的。

协议继承

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

  1. protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
  2. // protocol definition goes here
  3. }

这是TextRepresentable从上面继承协议的协议示例

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

本示例定义了一个新协议PrettyTextRepresentable,该协议继承自TextRepresentable凡是采用PrettyTextRepresentable必须满足所有的强制要求TextRepresentable加上通过强制执行的额外要求PrettyTextRepresentable在此示例中,PrettyTextRepresentable添加了一个单一要求以提供一个名为gettable的属性prettyTextualDescription,该属性返回a String

SnakesAndLadders级可扩展到通过并符合PrettyTextRepresentable

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

此扩展声明它采用PrettyTextRepresentable协议并提供prettyTextualDescriptionSnakesAndLadders类型属性的实现任何PrettyTextRepresentable还必须是TextRepresentable,因此start的实现prettyTextualDescription通过textualDescriptionTextRepresentable协议访问该属性以开始输出字符串开始。它附加一个冒号和一个换行符,并将其用作其漂亮文本表示的开始。然后,它遍历木板正方形的数组,并附加一个几何形状来表示每个正方形的内容:

  • 如果平方的值大于0,则它是阶梯的底,并由表示
  • 如果平方的值小于0,则为蛇的头,并用表示
  • 否则,平方的值为0,它是一个由表示的“自由”平方

prettyTextualDescription现在,属性可用于打印任何SnakesAndLadders实例的漂亮文字描述

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

纯类协议

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

  1. protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
  2. // class-only protocol definition goes here
  3. }

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

笔记

当该协议的要求定义的行为假设或要求符合类型具有引用语义而不是值语义时,请使用仅类协议。有关引用和值语义的更多信息,请参见结构和枚举是值类型,类是引用类型

协议组成

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

协议组成具有的形式您可以根据需要列出任意数量的协议,并用“&”号分隔除了协议列表之外,协议组成还可以包含一个类类型,您可以使用该类类型来指定所需的超类。SomeProtocol AnotherProtocol&

下面是结合了两个协议被调用的例子NamedAged到上的功能参数的单一协议组合物的要求:

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

在此示例中,Named协议对String名为的gettable属性具有单一要求nameAged协议对Int名为的可获取属性有单一要求age两种协议都被称为的结构采用Person

该示例还定义了一个wishHappyBirthday(to:)函数。celebrator参数的类型,表示“同时符合协议的任何类型”。只要将这两个特定的类型都符合两个必需的协议,就可以将哪种特定类型传递给该函数都没有关系。Named AgedNamedAged

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

这是一个将上一示例中的Named协议与一个Location结合在一起的示例

  1. class 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. class City: Location, Named {
  10. var name: String
  11. init(name: String, latitude: Double, longitude: Double) {
  12. self.name = name
  13. super.init(latitude: latitude, longitude: longitude)
  14. }
  15. }
  16. func beginConcert(in location: Location & Named) {
  17. print("Hello, \(location.name)!")
  18. }
  19. let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
  20. beginConcert(in: seattle)
  21. // Prints "Hello, Seattle!"

beginConcert(in:)函数采用type的参数,这意味着“属于协议的子类且符合协议的任何类型。” 在这种情况下,可以同时满足这两个要求。Location NamedLocationNamedCity

传递birthdayPersonbeginConcert(in:)函数无效,因为Person它不是的子类Location同样,如果您创建的子类Location不符合Named协议,则beginConcert(in:)使用该类型的实例进行调用也是无效的。

检查协议一致性

您可以使用类型转换中描述isas运算符来检查协议一致性,并转换为特定协议。检查并转换为协议的语法与检查并转换为类型的语法完全相同:

  • is运营商的回报true,如果一个实例符合协议并返回false,如果它不。
  • as?向下运算符版本返回协议类型的可选值,并且该值是nil实例不符合该协议的情况。
  • as!向下转换运算符版本将向下转换强制为协议类型,如果向下转换失败,则会触发运行时错误。

此示例定义了一个名为的协议HasArea该协议具有一个Double名为gettable的属性的单一属性要求area

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

这里有两个类,Circle并且Country,这两者的符合HasArea协议:

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

Circle类实现area性能要求作为一个计算的属性的基础上,所存储的radius属性。Country类实现了area直接需求的存储性能。这两个类均正确符合该HasArea协议。

这是一个名为的类Animal该类不符合该HasArea协议:

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

CircleCountryAnimal类没有共享的基类。尽管如此,它们都是类,因此可以使用这三种类型的实例来初始化一个存储类型为type的值的数组AnyObject

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

objects阵列被初始化为数组文本包含Circle具有2个单位的半径实例; 一个Country与英国平方公里的面积初始化实例; 还有一个Animal有四只脚实例。

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

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

每当数组中的对象符合HasArea协议时,as?操作员返回的可选值就会与可选绑定一起展开为一个名为的常量objectWithAreaobjectWithArea已知常量的类型为HasArea,因此area可以使用类型安全的方式访问和打印属性。

请注意,强制转换过程不会更改基础对象。它们继续是a Circle,aCountry和an Animal但是,在将它们存储在objectWithArea常量中这一点上,只知道它们是type HasArea,因此只能area访问它们的属性。

可选协议要求

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

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

可以使用可选链接调用可选协议要求,以解决未通过符合协议的类型实现要求的可能性。您可以通过在调用方法名称后写一个问号(例如)来检查可选方法的实现someOptionalMethod?(someArgument)有关可选链接的信息,请参见可选链接

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

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

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

笔记

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

Counter下面定义类具有dataSource类型的可选属性CounterDataSource?

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

Counter类存储在一个名为变量属性的当前值countCounter类也定义了一个称为方法increment,其中递增count每次方法调用时属性。

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

请注意,这里有两个级别的可选链接。首先,dataSource可能是nil可能是,所以dataSource在其名称后会有一个问号,表明increment(forCount:)只有在dataSourcenot时才应调用nil其次,即使dataSource 确实存在,也不能保证它实现increment(forCount:),因为这是可选要求。在这里,increment(forCount:)可能无法实现的可能性也通过可选的链接来处理。increment(forCount:)仅当increment(forCount:)存在时(即不存在时),才对to进行调用nil这就是为什么increment(forCount:)在名称后还会加上问号的原因。

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

调用之后,使用可选绑定将它返回increment(forCount:)的可选Int内容解包为一个称为的常量amount如果可选Int变量确实包含一个值(也就是说,如果委托和方法都存在,并且该方法返回了一个值),则将未包装的amount对象添加到存储的count属性中,并完成增量。

如果无法increment(forCount:)方法中检索值(由于dataSource为nil或由于数据源未实现),则increment(forCount:)increment()方法将尝试从数据源的fixedIncrement属性中检索值fixedIncrement属性也是可选要求,因此Int,即使协议定义fixedIncrement中将其定义为非可选Int属性,其值也是可选CounterDataSource

这是一个简单的CounterDataSource实现,其中数据源在3每次查询时都返回一个恒定值它通过实现可选fixedIncrement属性要求来做到这一点

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

您可以将的实例ThreeSource用作新Counter实例的数据源

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

上面的代码创建了一个新Counter实例;将其数据源设置为新ThreeSource实例;increment()四次调用计数器的方法。正如预期的那样,count每次increment()调用计数器的属性都会增加三倍

这是一个更复杂的数据源,称为TowardsZeroSource,它使Counter实例从其当前count向上或向下计数到零

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

TowardsZeroSource类实现可选的increment(forCount:)从方法CounterDataSource协议并使用该count参数值,以计算出到计数的方向。如果count已经是零,则该方法返回0到表示没有进一步的计数应该发生。

您可以将的实例TowardsZeroSource与现有Counter实例一起使用,从计数-4到零。一旦计数器达到零,就不再进行计数:

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

协议扩展

可以扩展协议以将方法,初始化程序,下标和计算属性实现提供给符合类型。这使您可以定义协议本身的行为,而不是每种类型的单独一致性或全局函数。

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

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

通过在协议上创建扩展,所有符合类型的类型都会自动获得此方法的实现,而无需进行任何其他修改。

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

协议扩展可以将实现添加到符合标准的类型,但不能使协议扩展或从另一个协议继承。协议继承始终在协议声明本身中指定。

提供默认实施

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

笔记

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

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

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

向协议扩展添加约束

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

例如,您可以定义Collection协议的扩展,该扩展适用于其元素符合Equatable协议的任何集合通过将集合的元素限制为Equatable协议(是标准库的一部分),可以使用==!=运算符检查两个元素之间的相等性和不相等性。

  1. extension 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. }

仅当集合中的所有元素均相等时,allEqual()方法才返回true

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

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

由于数组符合Collection,整数符合EquatableequalNumbers因此differentNumbers可以使用以下allEqual()方法:

  1. print(equalNumbers.allEqual())
  2. // Prints "true"
  3. print(differentNumbers.allEqual())
  4. // Prints "false"

笔记

如果符合类型满足提供相同方法或属性实现的多个受约束扩展的要求,则Swift将使用与最专门的约束相对应的实现。

 

 
posted @ 2018-10-03 14:48  为敢技术  阅读(768)  评论(0编辑  收藏  举报