The introduction
Continue learning about Swift documentation from the previous chapter: Extension, we learned about Swift extension, including declaring extensions using the extension keyword, adding attributes, instance methods, class methods, initializing methods, subscripts, defining new nested types, and making existing classes conform to the protocol. Now, let’s learn about the Swift protocol. Due to the long space, here is a section to record, next, let’s begin!
agreement
A protocol defines a blueprint for methods, attributes, and other requirements suitable for a particular task or block of functionality. Classes, structs, or enumerations can then adopt the protocol to provide an actual implementation of these requirements. Any type that meets the requirements of the protocol is said to comply with the protocol.
In addition to specifying the requirements that a consistent type must fulfill, you can extend the protocol to fulfill some of these requirements, or implement additional capabilities that a consistent type can take advantage of.
1 Protocol Syntax
Protocols are defined in the same way as classes, structs, and enumerations:
protocol SomeProtocol {
// protocol definition goes here
}
Copy the code
Custom types declare that they use a particular protocol by placing the protocol name after the type name (separated by colons) as part of their definition. Multiple protocols can be listed, separated by commas:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
Copy the code
If a class has a superclass, precede any protocol it adopts with the superclass name, followed by a comma:
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
Copy the code
2 Attribute Requirements
The protocol can require any consistent type to provide instance attributes or type attributes with a specific name and type. The protocol does not specify whether the property is stored or computed; it only specifies the desired property name and type. The protocol also specifies whether each attribute must be gettable or GEttable and settable.
If the protocol requires the property to be reachable and settable, the constant storage property or read-only computing property will not meet the property requirement. If the protocol requires only one property to be reachable, then any type of property will suffice, and if this is useful for your own code, then the property is also settable and valid.
Property requirements are always declared as variable properties prefixed with the var keyword. Gettable and setable attributes are represented by writing {get set} after the type declaration, and Gettable attributes are represented by writing {get}.
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
Copy the code
Always use the static keyword as a prefix when defining type attribute requirements in the protocol. This rule also applies to type attributes that require class or static keywords to be prefixed when implemented by a class:
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
Copy the code
Here is an example protocol with single-instance property requirements:
protocol FullyNamed {
var fullName: String { get }
}
Copy the code
The FullyNamed protocol requires a consistent type to provide a fully qualified name. The protocol does not specify any other properties of a consistent type, except that the type must be able to provide itself with a full name. The protocol states that any fullyName type must have a gettable instance attribute named fullName, which is of type String.
Here is an example of a simple structure that takes and complies with the FullyNamed protocol:
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"
Copy the code
This example defines a structure named Person that represents a specific named Person. It declares that it adopts the FullyNamed protocol as part of the first line of its definition. Each instance of Person has a storage property named fullName, which is of type String. This meets the single requirement of the FullyNamed protocol and means that Person has correctly complied with the protocol. (Swift reports errors at compile time if protocol requirements are not met.)
Here is a more complex class that also uses and complies with the FullyNamed protocol:
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"Copy the code
This class implements the fullName property requirement as a computational read-only property for starships. Each starship class instance stores a mandatory name and an optional prefix. The fullName attribute takes a prefix value (if one exists) and places it at the beginning of the name, creating a fullName for the starship.
3 Method Requirements
Protocols can require specific instance methods and type methods to be implemented by consistent types. These methods are written as part of the protocol definition in exactly the same way as normal instance and type methods, but without braces or method bodies. Mutable arguments are allowed, but subject to the same rules as the regular method. However, you cannot specify default values for method parameters in a protocol definition.
As with type attribute requirements, type method requirements are always prefixed with the static keyword when defined in the protocol. This is true even if a type method requires the class or static keyword to be prefixed when implemented by a class:
protocol SomeProtocol {
static func someTypeMethod()
}
Copy the code
Such as:
protocol RandomNumberGenerator {
func random() -> Double
}
Copy the code
This protocol, RandomNumberGenerator, requires that any consistent type have an instance method called Random, which returns a double value when called. Although it is not specified as part of the protocol, we assume that the value is a number ranging from 0.0 to 1.0 (but not including 1.0).
The RandomNumberGenerator protocol makes no assumptions about how each random number is generated, it simply requires the generator to provide a standard method for generating a new random number.
The following is an implementation of a class that uses and conforms to the RandomNumberGenerator protocol. This class implements a pseudo-random number generator algorithm called a linear congruence generator:
class LinearCongruentialGenerator: Var lastRandom = 42.0 let m = 139968.0 let a = 3877.0 let c = 29573.0 func random() -> Double {var lastRandom = 42.0 let m = 139968.0 let a = 3877.0 let c = 29573.0 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"Copy the code
4 Variable method requirements
Methods sometimes need to modify (or change) the instance to which they belong. For example, for instance methods of value types (that is, structs and enumerations), the mutating keyword can be placed before the func keyword of the method to indicate that the method is allowed to modify the instance to which it belongs and any attributes of that instance. This process is described in modifying a value type from within an instance method.
If you define a protocol instance method requirement that is intended to change any type of instance that adopts the protocol, mark the method with the mutating keyword in the protocol definition. This enables structures and enumerations to adopt protocols and satisfy method requirements.
Pay attention to
If you mark the protocol instance method requirement as mutating, you do not need to write the mutating keyword when writing the implementation of the method for the class. The mutating keyword is only used for structures and enumerations.
The following example defines a protocol called Togglable that defines a single-instance method requirement called toggle. As the name suggests, the toggle () method aims to switch or reverse the state of any consistent type, usually by modifying the properties of that type.
The toggle () method is marked as a mutating keyword as part of the Togglable protocol definition to indicate that the method will change the state of the consistent instance when it is called:
protocol Togglable {
mutating func toggle()
}
Copy the code
If a swappable protocol is implemented for a structure or enumeration, that structure or enumeration can comply with the protocol by providing an implementation of the toggle () method that is also marked mutable.
The following example defines an enumeration named OnOffSwitch. This enumeration toggles between two states, indicated by enabling and disabling enumeration cases. The enumeration’s toggle implementation is marked mutating to match the requirements of the Togglable protocol:
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
Copy the code
4 Initialization requirements
The protocol may require specific initializers to be implemented with consistent types. These initializers are written exactly like normal initializers as part of the protocol definition, but without braces or initializers:
protocol SomeProtocol {
init(someParameter: Int)
}
Copy the code
4.1 Class implementation of protocol initializer requirements
You can implement protocol initializer requirements on conformance classes, either as specified initializers or as convenient initializers. In both cases, the initializer implementation must be marked with the required modifiers:
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
Copy the code
Using the Required modifier ensures that explicit or inherited initializer requirement implementations are provided on all subclasses of the consistency class so that they are also compliant with the protocol.
For more information about required initializers, see Required Initializers.
Pay attention to
You do not need to use the required modifier token protocol initializer implementation on classes marked with final modifiers because final classes cannot be subclassed. For more information about the final modifier, see Preventing overwriting.
If the subclass overrides the initializer specified in the superclass and also implements the matching initializer requirement from the protocol, mark the initializer implementation with the Required and Override modifiers:
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
}
}
Copy the code
4.2 Failed initializer requirements
The protocol may define failure initializer requirements for consistency types, as defined in the failure initializer.
A failed initializer requirement can be satisfied by a failed or non-failed initializer on a consistent type. An unavailable initializer requirement can be satisfied by an unavailable initializer or an implicitly expanded failed initializer.
5 Protocol Type
The protocol itself does nothing. However, as a mature protocol, you can use mature types in your code. Using a protocol as a type is sometimes called an existential type, which comes from the phrase “there is a type T that makes T conform to a protocol.”
You can use protocols in many places where other types are allowed, including:
- As a parameter type or return type in a function, method, or initializer
- A type used as a constant, variable, or attribute
- As the type of an item in an array, dictionary, or other container
Pay attention to
Because protocols are types, their names start with a capital letter (such as FullyNamed and RandomNumberGenerator) to match other types of names in Swift (such as Int, String, and Double).
Such as:
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
}
}
Copy the code
This example defines a new class called Dice, which represents the N-sided Dice used in board games. Dice instances have an integer property called sides, which indicates how many sides they have, and a property called Generator, which provides a random number generator from which you can create dice roll values.
The generator property is of type RandomNumberGenerator. Therefore, you can set it to any type of instance using the RandomNumberGenerator protocol. There is no need to do anything on the instance assigned to this attribute except that the instance must use the RandomNumberGenerator protocol. Because its type is RandomNumberGenerator, the code in the Dice class can only interact with the generator in a way that applies to all generators that conform to this protocol. This means that it cannot use any methods or attributes defined by the generator’s underlying type. However, you can cast down from the protocol type to the underlying type in the same way that you can cast down from a parent class to a subclass, as described in the downcast.
Dice also has an initializer that sets its initial state. This initializer has a parameter named Generator, which is also of type RandomNumberGenerator. Any consistent type of value can be passed to this parameter when initializing a new Dice instance.
Dice provides an example method, roll, that returns an integer value between 1 and the number of Dice edges. This method calls the generator’s random () method to create a new random number between 0.0 and 1.0, and uses this random number to create the dice roll value within the correct range. Because the generator uses RandomNumberGenerator, a random () method is guaranteed to be called.
Below is the Dice class how to use linearcongreentialGenerator instance as a random number generator to create a six Dice:
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 4Copy the code
6 agent
Delegation is a design pattern that enables a class or structure to hand over (or delegate) some of its responsibilities to an instance of another type. This design pattern is implemented by defining a protocol that encapsulates the responsibilities of the delegate, such that a consistent type (called a delegate) is guaranteed to provide the functionality that has been delegated. Delegates can be used to respond to specific operations or to retrieve data from an external source without knowing the underlying type of that source. The following example defines two protocols for dice based board games:
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)
}
Copy the code
The dice Game protocol is a protocol that can be used for any game involving dice. The DiceGameDelegate protocol can be used to track the progress of dice games. To prevent strong reference loops, delegates are declared weak references. For information about weak references, see Strong reference loops between class instances. Marking a protocol as a class only allows the SnakesAndLadders class later in this chapter to declare that its delegate must use a weak reference. Pure class protocols are marked by inheritance from any object, as described in pure class protocols. This is a version of the snake and Ladder game originally introduced in Control Flow. This version applies to dice instances for dice rolling; Use dice game protocol; And notify the dice game of its progress:
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) } }Copy the code
For a description of the snake and ladder game, see Break.
This version of the game is packaged as a class called SnakesAndLadders, which uses the DiceGame protocol. It provides a gettable dice property and a play () method to conform to the protocol. (The DICE property is declared as a constant property because it does not need to be changed after initialization, and the protocol only requires that it be reachable.)
The Snakes and Ladders game board is set in the init () initializer of the class. All game logic is moved to the protocol’s Play method, which uses the dice property required by the protocol to provide the dice roll value.
Note that the delegate property is defined as the optional DiceGameDelegate, since no delegate is required to play the game. Because it is an optional type, the delegate property is automatically set to the initial value nil. After that, the game instantiator can choose to set the property to the appropriate delegate. Because the DiceGameDelegate protocol is class, you can declare the delegate to be weak to prevent reference loops.
DiceGameDelegate offers three ways to track your game’s progress. These three methods have been incorporated into the game logic in the play () method above and are called at the start of a new game, the start of a new turn, or the end of the game.
Because the delegate property is the optional DiceGameDelegate, the Play () method uses the optional link every time it calls a method on the delegate. If the delegate property is nil, these delegate calls will fail normally without error. If the delegate attribute is non-nil, the delegate method is called and passed as an argument to the SnakesAndLadders instance.
The next example shows a class called DiceGameTracker that uses the DiceGameDelegate protocol:
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")
}
}
Copy the code
DiceGameTracker implements all three methods required for DiceGameDelegate. It uses these methods to track the number of turns in a game. When the game starts, it resets the numberOfTurns property to zero, increments at the start of each new turn, and prints out the total numberOfTurns after the game ends.
The gameDidStart (:) implementation shown above uses the game parameter to print some introductory information about the game to be played. The game parameter is of type DiceGame, not SnakesAndLadders, so gameDidStart (:) can only access and use the methods and attributes implemented as part of the DiceGame protocol. However, the method can still use type conversions to query the type of the underlying instance. In this case, it checks if the game is actually an instance of snakes and ladders behind the scenes, and if so, prints an appropriate message.
The gameDidStart (:) method also accesses the dice property of the passed game parameters. Since the game is known to comply with the DiceGame protocol, it is guaranteed to have a dice attribute, so the gameDidStart (:) method can access and print the sides attribute of the dice, regardless of which game is being played.
Here’s what DiceGameTracker looks like in action:
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 turnsCopy the code
7 Use extensions to add protocol consistency
You can extend existing types to adopt and comply with new protocols, even if you don’t have access to the source code of an existing type. Extensions can add new attributes, methods, and subscripts to existing types, and therefore can add any requirements that a protocol might need. For more information about extensions, see Extensions.
Pay attention to
When an existing instance of a type is added to an instance’s type in an extension, existing instances of that type automatically adopt and comply with the protocol.
For example, this protocol, called TextRepresentable, can be implemented by any type that can be represented as text. This could be a description of itself, or a textual version of its current state:
protocol TextRepresentable {
var textualDescription: String { get }
}
Copy the code
The Dice class above can be extended to adopt and conform to TextRepresentable:
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
Copy the code
This extension adopts the new protocol in exactly the same way that Dice provided in its original implementation. The protocol name is provided after the type name, separated by a colon, and all required implementations of the protocol are provided in extended braces.
Any dice instance can now be treated as TextRepresentable:
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"
Copy the code
Similarly, the SnakesAndLadders game class can be extended to adopt and conform to the TextRepresentable protocol:
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"
Copy the code
7.1 Conditional compliance with this Agreement
A generic type can satisfy the protocol only under certain conditions, such as when the generic parameters of the type conform to the protocol. By listing the constraints when extending a generic type, you can make it conditionally conformed to the protocol. These constraints are written after the name of the protocol used by writing a generic WHERE clause. For more information about the generic WHERE clause, see the Generic WHERE clause.
The following extensions make array instances comply with the TextRepresentable protocol whenever they store elements that conform to the TextRepresentable type.
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]"
Copy the code
7.2 Using extensions to declare protocols
If a type already meets all the requirements of a protocol, but has not yet declared that it adopts the protocol, it can be made to adopt a protocol with an empty extension:
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
Copy the code
Now, as long as TextRepresentable is a required type, you can use an instance of Hamster:
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"
Copy the code
Pay attention to
A type does not automatically adopt a protocol simply by meeting its requirements. They must always use the protocol explicitly.
Implement the protocol using automatic composition
Swift can automatically provide protocol consistency for Equalable, Hashable, and Comparable in many simple cases. Using this composite implementation means that you don’t have to write repetitive boilerplate code to implement protocol requirements yourself.
Swift provides an automatic composition implementation of equalable for the following types of custom types:
- Structures that have only storage properties that conform to the Equalable protocol
- Only enumerations of association types that match the Equatable protocol
- There is no enumeration of associated types
To receive an automatic synthetic implementation of ==, declare consistency to Equatable in the file that contains the original declaration, rather than implementing the == operator yourself. The Equatable protocol provides a default implementation! =.
The following example defines a Vector3D structure of a three-dimensional position vector (x, y, z), similar to a Vector2D structure. Because the x, y, and Z attributes are all equivalent types, Vector3D receives an automatic synthetic implementation of the equivalent operators.
Struct Vector3D: Equatable {var x = 0.0, y = 0.0, z = 0.0} let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4) Let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: If twoThreeFour == anotherTwoThreeFour {print("These two vectors are also equivalent.")} // Prints "These two vectors are also equivalent."Copy the code
Swift provides an automatic composition implementation of Hashable for the following types of custom types:
- A structure that has only hash – compliant storage properties
- Only enumerations of association types that comply with the hash protocol
- There is no enumeration of associated types
To receive a synthetic implementation of hash (into :), declare consistency with Hashable in the file containing the original declaration rather than implementing the hash (into 🙂 method yourself.
Swift provides a comprehensive implementation of Comparable for enumerations that do not have raw values. If enumerations have associated types, they must all conform to the comparability protocol. To receive a synthesized implementation of <, declare consistency with Comparable in the file that contains the original enumeration declaration, rather than implementing the < operator yourself. The default implementations of the comparative protocol <=, >, and >= provide the remaining comparison operators.
The following example defines a SkillLevel enumeration for beginners, intermediates, and experts. In addition, experts rank them according to the number of stars they have.
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)"Copy the code
9 Protocol type set
Protocols can be used as types stored in collections such as arrays or dictionaries, as mentioned in protocols. This example creates an array of text representations:
let things: [TextRepresentable] = [game, d12, simonTheHamster]
Copy the code
We can now iterate over the items in the array and print a text description of each item:
for thing in things {
print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon
Copy the code
Notice the thing constant is of type TextRepresentable. It’s not a Dice, DiceGame, or Hamster type, even if the actual instance behind the scenes is one of those types. However, because its type is TextRepresentable, and any TextRepresentable has a textualDescription property, it is safe to access its thing. TextualDescription in a loop every time.
10 Protocol Inheritance
Protocols can inherit from one or more other protocols, and more requirements can be added on top of the inherited requirements. The syntax for protocol inheritance is similar to that for class inheritance, but you can choose to list multiple inherited protocols, separated by commas:
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// protocol definition goes here
}
Copy the code
Here is an example of inheriting the TextRepresentable protocol:
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}
Copy the code
This example defines a new protocol, PrettyTextRepresentable, which inherits TextRepresentable. Anything that uses PrettyTextRepresentable must meet all of the requirements that TextRepresentable implements, as well as additional requirements that PrettyTextRepresentable implements. In this case, PrettyTextRepresentable adds a requirement to provide a retrievable property called prettyTextualDescription, which returns a string.
The Snake Ladder class can be extended to adopt and conform to 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}}Copy the code
This extension states that it adopts the PrettyTextRepresentable protocol and provides an implementation of the prettyTextualDescription attribute for the SnakesAndLadders type. Any PrettyTextRepresentable must be TextRepresentable, So the implementation of prettyTextualDescription starts with an output string from the TextRepresentable protocol that accesses the textualDescription property. It appends a colon and a newline character as the beginning of a beautiful text presentation. It then iterates through an array of checkerboard squares and appends a geometric figure to represent the contents of each square:
- If the square value is greater than 0, it is the bottom of the ladder, denoted by â–².
- If the square has a value less than 0, it is the head of the snake, denoted by â–¼.
- Otherwise, the square has a value of 0, which is a “free” square, denoted by â—‹.
The prettyTextualDescription property can now be used to print a nice text description of any snake and ladder instance:
print(game.prettyTextualDescription) // A game of Snakes and Ladders with 25 squares: A. a. a. a. a. a. / / bring bring bring, bring about a. a. a. â–¼ a. a. a. a. a. a. â–¼ â–¼ â–¼"Copy the code
11 Class – Only agreement
By adding the AnyObject protocol to the inheritance list of protocols, you can limit protocol adoption to class types (rather than structures or enumerations).
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// class-only protocol definition goes here
}
Copy the code
In the example above, SomeClassOnlyProtocol is available only by class type. Writing a structure or enumeration definition that tries to adopt a ClassOnlyProtocol is a compile-time error.
Pay attention to
The class-only protocol is used when the behavior defined by protocol requirements assumes or requires consistency types to have referential semantics rather than value semantics. For more information on reference and value semantics, see Structs and enumerations are Value types, Classes are Reference types.
12 Protocol Composition
It is useful to require that a type conform to more than one protocol. You can combine multiple protocols into a single requirement through protocol composition. A combination of protocols behaves as if you defined a temporary local protocol that has the combination requirements of all the protocols in the combination. The protocol combination does not define any new protocol types.
Protocols can be formed in the form of SomeProtocol and AnotherProtocol. You can list as many protocols as you want, separating them with an ampersand (&). In addition to the list of protocols, the protocol composite can also contain a class type that you can use to specify the desired superclass.
Here is an example that combines two protocols Named Named and Aged into a protocol combination requirement for a function parameter:
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!"Copy the code
In this case, the naming protocol has a separate requirement for the reachable string attribute named Name. The Aged protocol has a separate requirement for an accessible Int attribute named Age. Both protocols are used by a structure called Person.
The example also defines a wishHappyBirthday (to 🙂 function. The type of the celebrator parameter is Named Named & Aged, which means “any type conforming to the Named and Aged protocols”. The particular type passed to a function is irrelevant, as long as it complies with both required protocols.
The example then creates a new Person instance named birthdayPerson and passes this new instance to the wishHappyBirthday (to 🙂 function. Since Person complies with both protocols, this call is valid, and the wishHappyBirthday (to 🙂 function can print its birthday message.
Here is an example combining the naming protocol from the previous example with the Location class:
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!"
Copy the code
The begincert (in 🙂 function takes an argument of type Location& Named, which means “any type that is a subclass of Location and conforms to the naming protocol.” In this case, City satisfies both requirements.
The birthdayPerson to BeginCert (in 🙂 function is not valid because Person is not a subclass of Location. Likewise, if you create a Location subclass that does not comply with the naming protocol, then calling beginocert (in 🙂 with instances of that type is also invalid.
13 Check protocol consistency
You can use the IS and AS operators described in type conversions to check protocol consistency and cast to a specific protocol. Checking a protocol and converting it to a protocol follows exactly the same syntax as checking a type and converting it to a type:
- The IS operator returns true if the instance complies with the protocol; If the instance does not conform to the protocol, return false.
- The as? The downcast operator returns an optional value of the protocol type, nil if the instance does not conform to that protocol.
- The as! The version of the downcast operator forces a downcast to a protocol type and fires a runtime error if the downcast fails.
This example defines a protocol named HasArea where one of the properties requires a reachable double property named area:
protocol HasArea {
var area: Double { get }
}
Copy the code
There are two classes, Circle and Country, that comply with the HasArea protocol:
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 } }Copy the code
The Circle class implements the area attribute requirement as a calculated attribute based on the stored RADIUS attribute. The Country class implements the area requirement directly as a storage property. Both classes correctly comply with the HasArea protocol.
Here is a class named Animal that does not comply with the HasArea protocol:
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
Copy the code
The Circle, Country, and Animal classes do not share a base class. However, they are all classes, so instances of all three types can be used to initialize arrays that store values of type AnyObject:
Let objects: [AnyObject] = [Circle(radius: 2.0), Country(area: 243_610), Animal(legs: 4)]Copy the code
The Objects array is initialized to an array text containing an instance of a Circle with a radius of 2 units; A Country instance initialized with the surface area of the United Kingdom (square kilometers); An instance of Animal with four legs.
We can now iterate over the objects array and check that each object in the array complies with the HasArea protocol:
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
Copy the code
Whenever an object in the array conforms to the HasArea protocol, as? The optional value operator returned, expanded by optional binding into a constant named objectWithArea. Given that the objectWithArea constant is of type HasArea, its area properties can be accessed and printed in a type-safe manner.
Note that the underlying object is not changed by the cast process. They’re still a Circle, a Country and an Animal. However, when they are stored in the objectWithArea constant, they are known to be of type HasArea, so only their area attribute can be accessed.
14 Optional protocol
You can define optional requirements for the protocol. These requirements need not be implemented by protocol-compliant types. As part of the protocol definition, it is optional to require that the optional modifier be prefixed. Optional requirements are available, so you can write code that interacts with Objective-C. Both protocols and optional requirements must be marked as @objc attributes. Note that the @objc protocol can only be adopted by classes that inherit from objective-C or other @objc classes. They cannot be taken by structs or enumerations.
When a method or property is used in an optional requirement, its type automatically becomes optional. For example, a method of type (Int) ->String becomes (Int) ->String? . Note that the entire function type is wrapped in the optional, not the method return value.
Optional protocol requirements can be invoked using optional links to illustrate the possibility that the requirement is not implemented by a protocol-conforming type. When a method is called, check the implementation of the alternative method by writing a question mark after the method name, such as someOptionalMethod? (someArgument). For information about optional links, see Optional Links.
The following example defines an integer counting class called Counter that provides its deltas using an external data source. This data source is defined by the CounterDataSource protocol, which has two optional requirements:
@objc protocol CounterDataSource {
@objc optional func increment(forCount count: Int) -> Int
@objc optional var fixedIncrement: Int { get }
}
Copy the code
The CounterDataSource protocol defines an optional method requirement called increment (forCount 🙂 and an optional attribute requirement called fixedIncrement. These requirements define two different ways for the data source to provide appropriate increments for the counter instance.
Pay attention to
Strictly speaking, you can write a custom class that conforms to CounterDataSource without implementing any of the protocol requirements. After all, they are optional. Although technically allowed, this is not a good data source.
The Counter class defined below has type CounterDataSource? Optional data source properties for:
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 } } }Copy the code
The Counter class stores its current value in a variable property named count. Increments properties are also referred to as “every time” methods.
The increment () method first tries to retrieve the increment by looking up an implementation of the increment (forCount 🙂 method on its data source. The increment () method uses the optional link to attempt to call increment (forCount :), passing the current count value as a single parameter to the method.
Note that there are two optional link levels. First, the dataSource may be nil, so there is a question mark after the dataSource name indicating that increment (forCount 🙂 should be called only if the dataSource is not nil. Second, even if the dataSource does exist, there is no guarantee that it implements increment (forCount :), since this is an optional requirement. Here, the possibility that increment (forCount 🙂 may not be implemented is also handled through optional links. The call to increment (forCount 🙂 occurs only if increment (forCount 🙂 exists, that is, if increment is not nil. That’s why increment (forCount 🙂 also has a question mark after the name.
Because the call to increment (forCount 🙂 may fail for one of two reasons, the call returns an optional Int value. This is true even if increment (forCount 🙂 is defined to return the non-optional Int value in the CounterDataSource definition. Even if there are two optional link operations, one after the other, the result is still wrapped in one optional. For more information about operating with multiple optional links, see Linking Multiple Levels of Links.
After calling increment (forCount :), the optional Int it returns expands to a constant called amount using the optional binding. If the optional Int contains a value, that is, if both the delegate and the method exist, and the method returns a value, the expanded amount is added to the count property of the store and the increment completes.
If the value cannot be retrieved from the increment (forCount 🙂 method, either because the data source is empty, or because the data source does not implement increment (forCount 🙂 – then the increment () method will attempt to retrieve the value from the fixedIncrement property of the data source. The fixedIncrement attribute is also an optional requirement, so its value is an optional Int value, even though fixedIncrement is defined as a non-optional Int attribute as part of the CounterDataSource protocol definition.
Here is a simple CounterDataSource implementation where the data source returns a constant value of 3 each time it is queried. It does this by implementing the optional fixedIncrement attribute requirement:
class ThreeSource: NSObject, CounterDataSource {
let fixedIncrement = 3
}
Copy the code
You can use ThreeSource’s instance as the data source for the new counter instance:
var counter = Counter() counter.dataSource = ThreeSource() for _ in 1... 4 { counter.increment() print(counter.count) } // 3 // 6 // 9 // 12Copy the code
The above code creates a new Counter instance; Set its data source to the new ThreeSource instance; Increment () of Counter is called four times. As expected, the count property of the counter increases by 3 each time increment () is called. Here is a more complex data source, TowardsZeroSource, which makes a counter instance count to zero up or down from its current count value:
class TowardsZeroSource: NSObject, CounterDataSource {
func increment(forCount count: Int) -> Int {
if count == 0 {
return 0
} else if count < 0 {
return 1
} else {
return -1
}
}
}
Copy the code
The TowardsZeroSource class implements the optional increment (forCount 🙂 method of the CounterDataSource protocol and uses the count parameter value to calculate in which direction to count. If count is already zero, the method returns 0 to indicate that no more counting should be done.
You can use an instance of TowardsZeroSource with an existing Counter instance, counting from -4 to zero. Once the counter reaches zero, the count is stopped:
counter.count = -4 counter.dataSource = TowardsZeroSource() for _ in 1... 5 { counter.increment() print(counter.count) } // -3 // -2 // -1 // 0 // 0Copy the code
15 Protocol Extension
The protocol can be extended to provide methods, initializers, subscripts, and computed property implementations to fit the required type. This allows you to define behavior on the protocol itself, rather than in separate conformance or global functions for each type.
For example, you can extend the RandomNumberGenerator protocol to provide a randomBool () method that returns a randomBool using the results of the desired Random () method:
Extension randomGenerator {func randomBool() -> Bool {return random() > 0.5}}Copy the code
By creating an extension on the protocol, all eligible types automatically get the implementation of the method without any additional modifications.
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"
Copy the code
Protocol extension can add implementations to a consistent type, but cannot extend or inherit from another protocol. Protocol inheritance is always specified in the protocol declaration itself.
15.1 Provides the default implementation
Protocol extensions can be used to provide a default implementation for any method or compute attribute requirements of the protocol. If the conformance type provides its own implementation of the required method or property, that implementation is used instead of the implementation provided by the extension.
Pay attention to
The default implementation provided by the extension has different protocol requirements than the optional protocol requirements. Although conformance types do not have to provide their own implementation, requirements with a default implementation can be invoked without an optional link.
For example, the PrettyTextRepresentable protocol, which inherits the TextRepresentable protocol, can provide a default implementation of the required prettyTextualDescription property, To simply return the result of accessing the textualDescription property:
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
Copy the code
15.2 Adding Constraints to Protocol Extensions
When defining a protocol extension, you can specify the constraints that a conformance type must satisfy before extended methods and properties can be made available. These constraints can be written after the name of the protocol to be extended by writing a generic WHERE clause. For more information about the generic WHERE clause, see the Generic WHERE clause.
For example, you can define an extension to the collection protocol that applies to any collection whose elements conform to the Equalable protocol. You can constrain it with part of a standard set! = The operator used to check whether two elements are equal.
extension Collection where Element: Equatable { func allEqual() -> Bool { for element in self { if element ! = self.first { return false } } return true } }Copy the code
The allEqual () method returns true only if all elements in the collection are equal.
Consider two arrays of integers, one with all elements identical and the other with different elements:
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
Copy the code
Since arrays conform to collections and integers conform to equalable, equalNumbers and differentNumbers can use the allEqual () method:
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"
Copy the code
Pay attention to
If the consistency type satisfies the requirement to extend multiple constraints that provide implementations for the same method or attribute, Swift uses the implementation corresponding to the most specific constraint.
Reference document: swift-protocols