FullyNamed
или RandomNumberGenerator
) для соответствия их имён типам в Swift (как например Int
, String
или Double
).
protocol RandomNumberGenerator {
func random() -> Double
}
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
}
}
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
имеют целочисленное свойство с названием sided
, которое представляет, сколько сторон имеет кубик, а также свойства generator
, который предоставляет генератор случайных чисел, который используется для создания значений бросков кубика.
generator
имеет тип RandomNumberGenerator
. Следовательно, Вы можете задать ему объект любого типа, который адаптирует RandomNumberGenerator
протокол. Ничего другого не требуется для присваиваемого Вами объекта этому свойству, кроме экземпляра, адаптирующего RandomNumberGenerator
протокол.
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 {
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}
DiceGame
- это протокол, который может быть адаптирован любой игрой, использующей игральный кубик. Протокол DiceGameDelegate
может быть адаптирован любым типом для отслеживания прогресса DiceGame
.
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
}
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)
}
}
Эта версия игры обёрнута в класс с названием SnakesAndLadders
, который адаптирует протокол DiceGame
. Он предоставляет получаемое свойство dic
и метод play()
для соответствия протоколу. (Свойство dice
/ объявлено как константное свойства, так как ему не нужно меняться посыле инициализации, а протокол лишь требует от него быть получаемым.)
init()
. Вся логика игры содержится в методе протокола play
, который использует требуемое протоколом свойство dice
для предоставления значений броска кубика.
delegate
определено как опциональное DiceGameDelegate
, так как делегат не требуется для игры в игру. Так как оно имеет опциональный тип, то свойство delegate
автоматически получает начальное значение nil
. Затем игра имеет возможность задать свойство в подходящий делегат.
DiceGameDelegate
предоставляет три метода для отслеживания прогресса игры. Эти три метода встроены в логику игры внутрь метода play()
выше и вызываются при старте игры, начале нового хода или по окончании игры.
delegate
является опционалом DiceGameDelegate
, то метод play()
использует опциональную цепочку каждый раз при его вызове над делегатом. Если свойство delegate
занулено, то эти вызовы делегата проваливаются без ошибок. Если свойство 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(_:)
также получает доступ к свойству dicе
/ у переданного параметра 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)
// Выведет "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)
// Выведет "A game of Snakes and Ladders with 25 squares"
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)
// Выведет "A hamster named Simon"
Типы не адаптируют протокол автоматически, просто удовлетворяя его требованиям. Они должны явно определять свою адаптивность всегда.
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
добавляет единственное требование для предоставления получаемого свойства с названием 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
, чтобы начать выходную строку. Он добавляет двоеточие и разрыв строки и использует это как начало милого текстового представления. Затем он итерируется через массив границ доски и добавляет геометрическую фигуру для представления содержимого каждого квадрата:
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
с названием name
. Он также определяет протокол с названием Aged
с единственным требованием на получаемое свойства Int
с названием age
. Оба этих протокола адаптируются структурой с названием Person
.
wishHappyBirthday(to:)
. Тип параметра сelebrator 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:)
с объектом этого типа так же будет некорректным.