Swift5.1 语言指南(二十三) 协议
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(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和可设置属性通过在类型声明后写入来指示,并且可通过写入来指示gettable属性。{ get set }
{ get }
- protocol SomeProtocol {
- var mustBeSettable: Int { get set }
- var doesNotNeedToBeSettable: Int { get }
- }
static
在协议中定义关键字时,始终使用关键字为类型属性要求添加前缀。即使在类实现时类型属性要求可以使用class
or static
关键字作为前缀,此规则也适用:
- 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
or static
关键字为前缀,也是如此:
- 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- sided骰子。Dice
实例有一个名为的整数属性sides
,它表示它们有多少边,以及一个名为的属性generator
,它提供了一个随机数生成器,用于创建骰子滚动值。
该generator
属性是类型RandomNumberGenerator
。因此,您可以将其设置为采用该协议的任何类型的实例RandomNumberGenerator
。除了实例必须采用RandomNumberGenerator
协议之外,您分配给此属性的实例不需要任何其他内容。因为它的类型是RandomNumberGenerator
,Dice
类中的代码只能以generator
适用于符合此协议的所有生成器的方式进行交互。这意味着它不能使用由生成器的基础类型定义的任何方法或属性。但是,可以从一个协议类型向下转换到在可以从一个超类向下转换到一个子类,如所讨论的相同方式从底层类型向下转换。
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
可以采用该协议来跟踪a的进度DiceGame
。为了防止强引用循环,委托被声明为弱引用。有关弱引用的信息,请参阅类实例之间的强引用循环。将协议标记为仅限类允许SnakesAndLadders
本章后面的类声明其委托必须使用弱引用。纯类协议由其继承标记,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)
- }
- }
有关Snakes and Ladders游戏的描述,请参阅Break。
这个版本的游戏被包装成一个名为的类SnakesAndLadders
,它采用DiceGame
协议。它提供gettable dice
属性和play()
方法以符合协议。(该dice
属性被声明为常量属性,因为它在初始化后不需要更改,并且协议只要求它必须是gettable。)
该蛇和梯子游戏板的设置采取类的内进行init()
初始化。所有游戏逻辑都被移入协议的play
方法,该方法使用协议的必需dice
属性来提供其骰子滚动值。
请注意,该delegate
属性被定义为可选 属性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
子句的更多信息,请参阅Generic 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"
注意
类型不会仅通过满足其要求自动采用协议。他们必须始终明确声明他们采用协议。
协议类型的集合
协议可以用作要存储在诸如数组或字典之类的集合中的类型,如Protocols as Types中所述。这个例子创建了一系列的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
返回一个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
所以prettyTextualDescription
通过textualDescription
从TextRepresentable
协议访问属性开始输出字符串开始实现。它附加冒号和换行符,并将其用作漂亮文本表示的开头。然后迭代通过棋盘方块的数组,并附加几何形状来表示每个方块的内容:
- 如果square的值大于
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
名为gettable的属性有一个要求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:)
函数采用类型的参数,这意味着“任何类型的子类,并且符合协议。”在这种情况下,满足这两个要求。Location & Named
Location
Named
City
传递birthdayPerson
给beginConcert(in:)
函数是无效的,因为Person
它不是子类Location
。同样,如果您创建了一个Location
不符合Named
协议的子类,则beginConcert(in:)
使用该类型的实例调用也是无效的。
检查协议一致性
您可以使用类型转换中描述的is
和as
运算符来检查协议一致性,以及转换为特定协议。检查并转换为协议遵循与检查和转换为类型完全相同的语法:
- 该
is
运算符返回true
如果一个实例遵循的协议,并返回false
,如果它不。 as?
downcast运算符的版本返回协议类型的可选值,nil
如果实例不符合该协议,则此值为。as!
如果向下转换不成功,则向下转换运算符的版本强制向下转换为协议类型并触发运行时错误。
此示例定义了一个名为的协议HasArea
,其中包含gettable Double
属性的单个属性要求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
类没有共享的基类。尽管如此,它们都是类,因此所有三种类型的实例都可用于初始化存储类型值的数组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
,a Country
和an Animal
。然而,在它们存储在objectWithArea
常量中时,它们只是已知类型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
不是时才应该调用nil
。其次,即使dataSource
不存在,也不能保证它实现了increment(forCount:)
,因为它是一个可选的要求。在这里,increment(forCount:)
可能未实现的可能性也由可选链接处理。调用increment(forCount:)
仅在increment(forCount:)
存在时发生- 即,如果不存在nil
。这就是为什么increment(forCount:)
在其名称后面还带有问号。
由于increment(forCount:)
这两个原因之一的调用可能会失败,因此调用返回一个可选 Int
值。即使increment(forCount:)
定义为在定义中返回非可选Int
值,也是如此CounterDataSource
。即使有两个可选的链接操作,一个接一个,结果仍然包含在一个可选的。有关使用多个可选链接操作的详细信息,请参阅链接多个链接级别。
在调用之后,它返回increment(forCount:)
的可选项Int
将被解包为amount
使用可选绑定调用的常量。如果optional Int
包含一个值 - 即,如果委托和方法都存在,并且该方法返回一个值 - 则将unwrapped amount
添加到stored 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()
调用计数器的属性增加3 。
这是一个更复杂的数据源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
0 开始计数。一旦计数器达到零,就不再进行计数:
- 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
子句的更多信息,请参阅Generic 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使用与最专用约束相对应的实现。
转载于:https://www.cnblogs.com/strengthen/p/9739783.html
Swift5.1 语言指南(二十三) 协议相关推荐
- Swift5.1 语言指南(二十) 类型转换
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ ➤微信公众号:山青咏芝(shanqingyongzhi) ➤博客园地址:山青咏芝(https://www.cnblog ...
- Swift语言指南(二)--语言基础之注释和分号
Swift语言指南(二)--语言基础之注释和分号 原文:Swift语言指南(二)--语言基础之注释和分号 注释 通过注释向自己的代码中注入不可执行的文本,作为你自己的笔记或提示.Swift编译器运行时 ...
- Swift5.1 语言指南(十三) 方法
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ ➤微信公众号:山青咏芝(shanqingyongzhi) ➤博客园地址:山青咏芝(https://www.cnblog ...
- C语言试题二十三之编写一个函数void function(int tt[m][n],int pp[n]),tt指向一个m行n列的二维函数组,求出二维函数组每列中最小元素,并依次放入pp所指定一维数组中
1. 题目 请编写一个函数void function(int tt[m][n],int pp[n]),tt指向一个m行n列的二维函数组,求出二维函数组每列中最小元素,并依次放入pp所指定一维数组中.二 ...
- Swift5.1 语言指南(三) 快速之旅
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ ➤微信公众号:山青咏芝(shanqingyongzhi) ➤博客园地址:山青咏芝(https://www.cnblog ...
- Swift5.1 语言指南(一) 关于Swift
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ ➤微信公众号:山青咏芝(shanqingyongzhi) ➤博客园地址:山青咏芝(https://www.cnblog ...
- Go语言学习 二十三 错误处理和运行时恐慌(Panic)
本文最初发表在我的个人博客,查看原文,获得更好的阅读体验 一 错误 1.1 error类型 按照约定,Go中的错误类型为error,这是一个内建接口,nil值表示没有错误: type error in ...
- Swift5.1 语言指南(九) 闭包
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ ➤微信公众号:山青咏芝(shanqingyongzhi) ➤博客园地址:山青咏芝(https://www.cnblog ...
- C语言(二十三)auto自动变量
auto自动变量 1.自动变量,只有定义他们的时候才创建,在定义他们的函数返回时,系统回收变量所占存储空间,对于自动变量的存储空间的分配与回收是由系统自动完成的.一般情况下,不作专门说明的局部变量均是 ...
最新文章
- campus bike at stanford
- Repeating Cipher
- MYSQL查询空值/NULL值
- idea 一直在build_让web开发部署提速 8 倍的一款 IDEA 插件,你有在用?
- Java中的AES加密和解密(CBC模式)
- laravel 异常捕获_Laravel框架捕获各种类型错误
- java虚拟机内存模型与垃圾回收知识复习总结
- 类ThreadLocal的使用与源码分析
- 信号与系统与数字信号处理丹梅老师公众号笔记
- 一般的java项目后台都有什么技术?
- python:算术平方根的实现
- DREAMWERVER CS5序列号反复验证 解决方法
- 升级Win11后,语言栏不在任务栏上
- 三种存储类型:块存储、文件存储、对象存储
- 二维码扫描枪是如何在我们生活中助力移动支付成为主流
- Tableau——方向图标的应用
- [linux kernel] 内核下ksz8081驱动调试
- 安装WampServer后无法打开localhost的问题
- 将html页面中部分div 导出为word ,纯前端处理,解决word导出视图 问题
- 【luoguP5550】Chino的数列