Swift5.4 语言指南(二十三) 协议
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(shanqingyongzhi)
➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/)
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址:https://www.cnblogs.com/strengthen/p/9739783.html
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
甲协议定义的该适合特定任务或片的功能的方法,属性和其他要求的蓝图。该协议然后可以采用由一个类,结构,或枚举,以提供实际实施方案的这些要求。满足协议要求的任何类型都被称为符合该协议。
除了指定必须符合标准的类型的要求之外,您还可以扩展协议以实现这些要求中的某些要求,或者实施符合标准的类型可以利用的其他功能。
协议语法
您以与类,结构和枚举非常相似的方式定义协议:
- protocol SomeProtocol {
- // protocol definition goes here
- }
自定义类型声明它们采用特定的协议,方法是将协议名称放在类型名称之后,并用冒号分隔,以作为其定义的一部分。可以列出多个协议,并用逗号分隔:
- struct SomeStructure: FirstProtocol, AnotherProtocol {
- // structure definition goes here
- }
如果一个类具有超类,请在其采用的任何协议之前列出超类名称,并以逗号开头:
- class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
- // class definition goes here
- }
物业要求
协议可以要求任何符合条件的类型来提供具有特定名称和类型的实例属性或类型属性。协议没有指定该属性是存储属性还是计算属性,仅指定所需的属性名称和类型。该协议还指定每个属性必须是可获取的还是可获取的和可设置的。
如果协议要求某个属性必须是可获取和可设置的,则该属性的要求不能由常量存储的属性或只读的计算属性来满足。如果协议仅要求一个属性是可获取的,则该要求可以由任何种类的属性来满足,并且对于可用于您自己的代码的属性,也可以对其进行设置是有效的。
属性要求始终声明为变量属性,并以var
关键字为前缀。gettable和settable属性在类型声明后通过写来指示,而gettable属性通过write来指示。{ get set }
{ get }
- protocol SomeProtocol {
- var mustBeSettable: Int { get set }
- var doesNotNeedToBeSettable: Int { get }
- }
static
在协议中定义类型属性要求时,请始终在其前面加上关键字。即使类型属性要求在由类实现时可以以class
orstatic
关键字作为前缀,该规则也适用:
- protocol AnotherProtocol {
- static var someTypeProperty: Int { get set }
- }
这是一个具有单实例属性要求的协议示例:
- protocol FullyNamed {
- var fullName: String { get }
- }
该FullyNamed
协议需要一个符合标准的类型来提供完全限定的名称。该协议未指定有关符合类型的性质的任何其他信息,仅指定该类型必须能够为其自身提供全名。该协议规定,任何FullyNamed
类型必须有称为gettable实例属性fullName
,它的类型的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
属性要求实现为飞船的已计算的只读属性。每个Starship
类实例存储一个必填name
和一个可选的prefix
。该fullName
属性使用该prefix
值(如果存在),并将其添加到的开头name
以为星际飞船创建全名。
方法要求
协议可能要求特定的实例方法和类型方法要通过符合类型来实现。这些方法以与普通实例和类型方法完全相同的方式编写为协议定义的一部分,但没有花括号或方法主体。可变参数是允许的,但要遵循与常规方法相同的规则。但是,不能在协议的定义中为方法参数指定默认值。
与类型属性要求一样,static
在协议中定义类型方法要求时,请始终在其前面加上关键字。即使类型方法要求在由类实现时以class
orstatic
关键字为前缀也是如此:
- protocol SomeProtocol {
- static func someTypeMethod()
- }
以下示例定义了一个具有单实例方法要求的协议:
- protocol RandomNumberGenerator {
- func random() -> Double
- }
该协议RandomNumberGenerator
要求任何符合条件的类型都有一个称为的实例方法random
,该实例方法在被调用时会返回一个Double
值。尽管未将其指定为协议的一部分,但假设此值是从0.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"
变异方法要求
有时有必要使用一种方法来修改(或变异)它所属的实例。对于值类型(即结构和枚举)的实例方法,请将mutating
关键字放在方法的func
关键字之前,以指示允许该方法修改其所属的实例以及该实例的任何属性。在实例方法中修改值类型中介绍了此过程。
如果您定义了一个协议实例方法要求,该要求旨在使采用该协议的任何类型的实例发生变异,请将该方法标记为mutating
关键字,作为协议定义的一部分。这使结构和枚举可以采用该协议并满足该方法要求。
笔记
如果将协议实例方法的要求标记为mutating
,则mutating
在为类编写该方法的实现时不需要编写关键字。该mutating
关键字仅由结构和枚举。
下面的示例定义了一个名为的协议Togglable
,该协议定义了一个名为的单实例方法要求toggle
。顾名思义,该toggle()
方法旨在切换或反转任何符合类型的状态,通常是通过修改该类型的属性来实现。
在协议定义中,toggle()
用mutating
关键字标记该方法Togglable
,以表明该方法在被调用时将使符合实例的状态发生变化:
- protocol Togglable {
- mutating func toggle()
- }
如果您Togglable
为某个结构或枚举实现协议,则该结构或枚举可以通过提供该toggle()
方法的实现(也标记为)来符合该协议mutating
。
以下示例定义了一个名为的枚举OnOffSwitch
。此枚举在两个状态之间切换,由枚举用on
和表示off
。枚举的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
初始化程序要求
协议可能要求特定的初始化程序通过一致的类型来实现。您可以使用与普通初始化程序完全相同的方式将这些初始化程序编写为协议定义的一部分,但不使用花括号或初始化程序主体:
- protocol SomeProtocol {
- init(someParameter: Int)
- }
协议初始化程序要求的类实现
您可以在符合条件的类上实现协议初始化程序要求,既可以将其指定为初始化程序,也可以作为便捷初始化程序。在这两种情况下,都必须使用required
修饰符标记初始化程序的实现:
- class SomeClass: SomeProtocol {
- required init(someParameter: Int) {
- // initializer implementation goes here
- }
- }
使用required
修饰符可确保您在符合类的所有子类上提供初始化程序要求的显式或继承实现,以使它们也符合协议。
有关必需的初始化程序的更多信息,请参见必需的初始化程序。
笔记
您不需要在用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
- }
- }
初始化器失败要求
协议可以为一致性类型定义失败的初始化器要求,如Failable Initializers中所定义。
合格的初始化器要求可以由合格或不合格的初始化器类型满足。不可失败的初始化器或隐式展开的可失败初始化器可以满足不可失败的初始化器要求。
协议作为类型
协议本身实际上并没有实现任何功能。但是,您可以将协议用作代码中的完整类型。将协议用作类型有时有时称为存在类型,它来自短语“存在类型T,使得T符合协议”。
您可以在允许使用其他类型的许多地方使用协议,包括:
- 作为函数,方法或初始化程序中的参数类型或返回类型
- 作为常量,变量或属性的类型
- 作为数组,字典或其他容器中项目的类型
笔记
由于协议的类型,开始他们的名称以大写字母(如FullyNamed
和RandomNumberGenerator
),以配合其他类型的雨燕的名称(如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面骰子。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
随机实例生成器的六面骰子的方法:
- 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
代表团
委托是一种设计模式,使类或结构可以将其某些职责移交给(或委托)另一种类型的实例。通过定义封装委托职责的协议来实现此设计模式,从而确保符合类型(称为委托)可以提供已委托的功能。委托可用于响应特定操作,或从外部源检索数据而无需了解该源的基础类型。
下面的示例定义了两种用于基于骰子的棋盘游戏的协议:
- 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)
- }
该DiceGame
协议是可以被涉及骰子的任何游戏采用的协议。
该DiceGameDelegate
协议可以用来跟踪进度DiceGame
。为防止强引用循环,将委托声明为弱引用。有关弱引用的信息,请参见类实例之间的强引用循环。将协议标记为仅类,可以使SnakesAndLadders
本章稍后的类声明其委托必须使用弱引用。只有A类的协议是通过从它的继承标记AnyObject
,如在讨论类只有协议。
这是最初在Control Flow中引入的Snakes and Ladders游戏的一个版本。该版本适用于其骰子实例;采纳该协议;并通知其进度:Dice
DiceGame
DiceGameDelegate
- 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
协议。它提供了一个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
协议:
- 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
的协议。但是,该方法仍然可以使用类型转换来查询基础实例的类型。在此示例中,它检查game
实际上是否是SnakesAndLadders
幕后实例,如果是,则打印适当的消息。
该gameDidStart(_:)
方法还访问dice
传递的game
参数的属性。由于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
通过扩展添加协议一致性
即使您无权访问现有类型的源代码,也可以扩展现有类型以采用并遵循新协议。扩展可以向现有类型添加新的属性,方法和下标,因此可以添加协议可能要求的任何要求。有关扩展的更多信息,请参见扩展。
笔记
当在扩展中将该一致性添加到实例的类型时,该类型的现有实例会自动采用并符合协议。
例如,此协议称为TextRepresentable
,可以通过任何一种可以表示为文本的方式来实现。这可能是对自身的描述,也可能是其当前状态的文本版本:
- protocol TextRepresentable {
- var textualDescription: String { get }
- }
Dice
上面的类可以扩展为采用和遵循TextRepresentable
:
- extension Dice: TextRepresentable {
- var textualDescription: String {
- return "A \(sides)-sided dice"
- }
- }
此扩展采用新协议的方式与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"
有条件地遵守协议
通用类型仅在某些条件下(例如,当该类型的通用参数符合该协议时)才能够满足协议的要求。您可以通过在扩展类型时列出约束来使泛型类型有条件地符合协议。通过编写一个通用where
子句,在要采用的协议名称之后编写这些约束。有关泛型where
子句的更多信息,请参见泛型子句。
以下扩展使Array
实例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]"
声明协议采用扩展
如果类型已经符合协议的所有要求,但尚未声明采用该协议,则可以使它采用带有空扩展名的协议:
- struct Hamster {
- var name: String
- var textualDescription: String {
- return "A hamster named \(name)"
- }
- }
- extension Hamster: TextRepresentable {}
Hamster
现在可以TextRepresentable
在所需类型的任何地方使用的实例:
- let simonTheHamster = Hamster(name: "Simon")
- let somethingTextRepresentable: TextRepresentable = simonTheHamster
- print(somethingTextRepresentable.textualDescription)
- // Prints "A hamster named Simon"
笔记
类型不会仅通过满足协议的要求就自动采用协议。他们必须始终明确声明其对协议的采用。
通过综合实现采用协议
斯威夫特可以自动提供协议一致性的Equatable
,Hashable
以及Comparable
在很多简单的情况。使用这种综合的实现意味着您不必编写重复的样板代码即可自己实现协议要求。
Swift提供了Equatable
以下几种自定义类型的综合实现:
- 仅存储符合
Equatable
协议属性的结构 - 仅具有符合
Equatable
协议的关联类型的枚举 - 没有关联类型的枚举
要接收的综合实现==
,请Equatable
在包含原始声明的文件中声明对的符合性,而无需==
自己实现运算符。该Equatable
协议提供的默认实现!=
。
以下示例Vector3D
为三维位置矢量定义了一种结构,类似于该结构。因为,和性能都是一个的类型,接收合成的等价运营商的实现。(x, y, z)
Vector2D
x
y
z
Equatable
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
以下几种自定义类型的综合实现:
- 仅存储符合
Hashable
协议属性的结构 - 仅具有符合
Hashable
协议的关联类型的枚举 - 没有关联类型的枚举
要接收的综合实现hash(into:)
,请Hashable
在包含原始声明的文件中声明对的符合性,而无需hash(into:)
自己实现方法。
Swift为Comparable
没有原始值的枚举提供了的综合实现。如果枚举具有关联的类型,则它们必须全部符合Comparable
协议。要接收的综合实现<
,请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)"
协议类型的集合
协议可用作要存储在诸如数组或字典之类的集合中的类型,如“协议作为类型”中所述。这个例子创建了一个TextRepresentable
东西数组:
- 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
每次通过循环访问都是安全的。
协议继承
一个协议可以继承一个或多个其他协议,并且可以在继承的要求之上添加更多要求。协议继承的语法类似于类继承的语法,但是可以选择列出多个继承的协议,并用逗号分隔:
- protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
- // protocol definition goes here
- }
这是TextRepresentable
从上面继承协议的协议示例:
- protocol PrettyTextRepresentable: TextRepresentable {
- var prettyTextualDescription: String { get }
- }
本示例定义了一个新协议PrettyTextRepresentable
,该协议继承自TextRepresentable
。凡是采用PrettyTextRepresentable
必须满足所有的强制要求TextRepresentable
,加上通过强制执行的额外要求PrettyTextRepresentable
。在此示例中,PrettyTextRepresentable
添加了一个单一要求以提供一个名为gettable的属性prettyTextualDescription
,该属性返回a String
。
该SnakesAndLadders
级可扩展到通过并符合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
协议并提供prettyTextualDescription
该SnakesAndLadders
类型的属性的实现。任何PrettyTextRepresentable
还必须是TextRepresentable
,因此start的实现prettyTextualDescription
通过textualDescription
从TextRepresentable
协议访问该属性以开始输出字符串开始。它附加一个冒号和一个换行符,并将其用作其漂亮文本表示的开始。然后,它遍历木板正方形的数组,并附加一个几何形状来表示每个正方形的内容:
- 如果平方的值大于
0
,则它是阶梯的底,并由表示▲
。 - 如果平方的值小于
0
,则为蛇的头,并用表示▼
。 - 否则,平方的值为
0
,它是一个由表示的“自由”平方○
。
prettyTextualDescription
现在,该属性可用于打印任何SnakesAndLadders
实例的漂亮文字描述:
- print(game.prettyTextualDescription)
- // A game of Snakes and Ladders with 25 squares:
- // ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
纯类协议
通过将AnyObject
协议添加到协议的继承列表中,可以将协议采用限制为类类型(而不是结构或枚举)。
- protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
- // class-only protocol definition goes here
- }
在上面的示例中,SomeClassOnlyProtocol
只能由类类型采用。编写尝试采用的结构或枚举定义是编译时错误SomeClassOnlyProtocol
。
笔记
当该协议的要求定义的行为假设或要求符合类型具有引用语义而不是值语义时,请使用仅类协议。有关引用和值语义的更多信息,请参见结构和枚举是值类型,而类是引用类型。
协议组成
要求一种类型同时符合多种协议可能很有用。您可以将多个协议组合成具有协议组成的单个需求。协议组合的行为就像您定义了一个临时本地协议,该协议具有组合中所有协议的组合要求。协议组成未定义任何新的协议类型。
协议组成具有的形式。您可以根据需要列出任意数量的协议,并用“&”号分隔。除了协议列表之外,协议组成还可以包含一个类类型,您可以使用该类类型来指定所需的超类。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!"
在此示例中,Named
协议对String
名为的gettable属性具有单一要求name
。该Aged
协议对Int
名为的可获取属性有单一要求age
。两种协议都被称为的结构采用Person
。
该示例还定义了一个wishHappyBirthday(to:)
函数。celebrator
参数的类型为,表示“同时符合和协议的任何类型”。只要将这两个特定的类型都符合两个必需的协议,就可以将哪种特定类型传递给该函数都没有关系。Named & Aged
Named
Aged
然后,该示例创建一个Person
名为的新实例birthdayPerson
,并将该新实例传递给该wishHappyBirthday(to:)
函数。因为Person
符合这两个协议,所以此调用有效,并且该wishHappyBirthday(to:)
函数可以打印其生日问候。
这是一个将上一示例中的Named
协议与一个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!"
该beginConcert(in:)
函数采用type的参数,这意味着“属于协议的子类且符合协议的任何类型。” 在这种情况下,可以同时满足这两个要求。Location & Named
Location
Named
City
传递birthdayPerson
给beginConcert(in:)
函数无效,因为Person
它不是的子类Location
。同样,如果您创建的子类Location
不符合Named
协议,则beginConcert(in:)
使用该类型的实例进行调用也是无效的。
检查协议一致性
您可以使用类型转换中描述的is
和as
运算符来检查协议一致性,并转换为特定协议。检查并转换为协议的语法与检查并转换为类型的语法完全相同:
- 该
is
运营商的回报true
,如果一个实例符合协议并返回false
,如果它不。 as?
向下运算符的版本返回协议类型的可选值,并且该值是nil
实例不符合该协议的情况。as!
向下转换运算符的版本将向下转换强制为协议类型,如果向下转换失败,则会触发运行时错误。
此示例定义了一个名为的协议HasArea
,该协议具有一个Double
名为gettable的属性的单一属性要求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
类实现area
性能要求作为一个计算的属性的基础上,所存储的radius
属性。本Country
类实现了area
直接需求的存储性能。这两个类均正确符合该HasArea
协议。
这是一个名为的类Animal
,该类不符合该HasArea
协议:
- class Animal {
- var legs: Int
- init(legs: Int) { self.legs = legs }
- }
的Circle
,Country
而Animal
类没有共享的基类。尽管如此,它们都是类,因此可以使用这三种类型的实例来初始化一个存储类型为type的值的数组AnyObject
:
- let objects: [AnyObject] = [
- Circle(radius: 2.0),
- Country(area: 243_610),
- Animal(legs: 4)
- ]
该objects
阵列被初始化为数组文本包含Circle
具有2个单位的半径实例; 一个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
可以使用类型安全的方式访问和打印其属性。
请注意,强制转换过程不会更改基础对象。它们继续是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
协议定义,该协议有两个可选要求:
- @objc protocol CounterDataSource {
- @objc optional func increment(forCount count: Int) -> Int
- @objc optional var fixedIncrement: Int { get }
- }
该CounterDataSource
协议定义了一个称为的可选方法要求increment(forCount:)
和名为的一个可选属性要求fixedIncrement
。这些要求定义了两种不同的方式,供数据源为Counter
实例提供适当的增量。
笔记
严格来说,您可以编写一个符合的自定义类,而CounterDataSource
无需实现任何协议要求。毕竟,它们都是可选的。尽管在技术上允许,但这并不能构成一个很好的数据源。
Counter
下面定义的类具有dataSource
类型的可选属性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
。的Counter
类也定义了一个称为方法increment
,其中递增count
每次方法调用时属性。
该increment()
方法首先尝试通过increment(forCount:)
在其数据源上查找该方法的实现来检索增量。该increment()
方法使用可选的链接尝试调用increment(forCount:)
,并将当前count
值作为方法的单个参数传递。
请注意,这里有两个级别的可选链接。首先,dataSource
可能是nil
,可能是,所以dataSource
在其名称后会有一个问号,表明increment(forCount:)
只有在dataSource
not时才应调用它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
属性要求来做到这一点:
- class ThreeSource: NSObject, CounterDataSource {
- let fixedIncrement = 3
- }
您可以将的实例ThreeSource
用作新Counter
实例的数据源:
- var counter = Counter()
- counter.dataSource = ThreeSource()
- for _ in 1...4 {
- counter.increment()
- print(counter.count)
- }
- // 3
- // 6
- // 9
- // 12
上面的代码创建了一个新Counter
实例;将其数据源设置为新ThreeSource
实例;并increment()
四次调用计数器的方法。正如预期的那样,count
每次increment()
调用计数器的属性都会增加三倍。
这是一个更复杂的数据源,称为TowardsZeroSource
,它使Counter
实例从其当前count
值向上或向下计数到零:
- 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
类实现可选的increment(forCount:)
从方法CounterDataSource
协议并使用该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
协议扩展
可以扩展协议以将方法,初始化程序,下标和计算属性实现提供给符合类型。这使您可以定义协议本身的行为,而不是每种类型的单独一致性或全局函数。
例如,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"
协议扩展可以将实现添加到符合标准的类型,但不能使协议扩展或从另一个协议继承。协议继承始终在协议声明本身中指定。
提供默认实施
您可以使用协议扩展为该协议的任何方法或计算的属性要求提供默认实现。如果符合类型提供了自己的所需方法或属性的实现,则将使用该实现而不是扩展提供的实现。
笔记
扩展提供的具有默认实现的协议要求与可选协议要求不同。尽管一致类型不必提供它们自己的实现,但是可以在没有可选链接的情况下调用具有默认实现的需求。
例如,PrettyTextRepresentable
继承TextRepresentable
协议的协议可以提供其必需prettyTextualDescription
属性的默认实现,以简单地返回访问该textualDescription
属性的结果:
- extension PrettyTextRepresentable {
- var prettyTextualDescription: String {
- return textualDescription
- }
- }
向协议扩展添加约束
定义协议扩展时,可以指定在扩展的方法和属性可用之前必须符合的类型的约束。您可以通过编写泛型where
子句在要扩展的协议名称后编写这些约束。有关泛型where
子句的更多信息,请参见泛型子句。
例如,您可以定义Collection
协议的扩展,该扩展适用于其元素符合Equatable
协议的任何集合。通过将集合的元素限制为Equatable
协议(是标准库的一部分),可以使用==
和!=
运算符检查两个元素之间的相等性和不相等性。
- 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]
由于数组符合Collection
,整数符合Equatable
,equalNumbers
因此differentNumbers
可以使用以下allEqual()
方法:
- print(equalNumbers.allEqual())
- // Prints "true"
- print(differentNumbers.allEqual())
- // Prints "false"
笔记
如果符合类型满足提供相同方法或属性实现的多个受约束扩展的要求,则Swift将使用与最专门的约束相对应的实现。