Unetway

Swift - Протоколы

Протокол определяет план методов, свойств и других требований , которые подходят для конкретной задачи или часть функциональности. Затем протокол может быть принят классом, структурой или перечислением, чтобы обеспечить фактическую реализацию этих требований. Говорят, что любой тип, который удовлетворяет требованиям протокола, соответствует этому протоколу.

Помимо указания требований, которые должны реализовывать соответствующие типы, вы можете расширить протокол для реализации некоторых из этих требований или для реализации дополнительных функций, которыми могут воспользоваться соответствующие типы.

Синтаксис протокола

Вы определяете протоколы очень похоже на классы, структуры и перечисления:

protocol SomeProtocol {

// protocol definition goes here

}

Пользовательские типы заявляют, что они принимают определенный протокол, помещая имя протокола после имени типа, разделенного двоеточием, как часть их определения. Несколько протоколов могут быть перечислены и разделены запятыми:

struct SomeStructure: FirstProtocol, AnotherProtocol {

// structure definition goes here

}

Если у класса есть суперкласс, перечислите имя суперкласса перед любыми протоколами, которые он принимает, а затем запятую:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {

// class definition goes here

}

Требования к недвижимости

Протокол может требовать любого соответствующего типа для предоставления свойства экземпляра или свойства типа с определенным именем и типом. Протокол не указывает, должно ли свойство быть хранимым или вычисляемым свойством - он только указывает требуемое имя и тип свойства. Протокол также указывает, должно ли каждое свойство быть gettable или gettable и settable.

Если протокол требует, чтобы свойство было доступным и устанавливаемым, это требование свойства не может быть выполнено с помощью постоянного хранимого свойства или вычисляемого свойства только для чтения. Если протокол требует только, чтобы свойство было доступным, требование может быть удовлетворено любым свойством, и свойство также может быть установлено, если это полезно для вашего собственного кода.

Требования к свойствам всегда объявляются как переменные свойства с префиксом varключевого слова. Свойства gettable и settable указываются записью после объявления их типа, а свойства gettable указываются записью .{ get set }{ get }

protocol SomeProtocol {

var mustBeSettable: Int { get set }

var doesNotNeedToBeSettable: Int { get }

}

Всегда staticопределяйте префиксные требования к свойствам типа с ключевым словом, когда вы определяете их в протоколе. Это правило относится, даже если требования к свойствам типа могут быть добавлены с ключевым словом classor 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и необязательный prefixfullNameСвойство использует prefixзначение , если оно существует, и присоединяет его к началу , nameчтобы создать полное имя для звездолета.

Требования к методу

Протоколы могут требовать реализации конкретных методов экземпляров и методов типов с помощью соответствующих типов. Эти методы пишутся как часть определения протокола точно так же, как для обычных методов экземпляра и типа, но без фигурных скобок или тела метода. Допускаются переменные параметры, подчиняющиеся тем же правилам, что и для обычных методов. Однако значения по умолчанию не могут быть указаны для параметров метода в определении протокола.

Как и в случае с требованиями к свойствам типа, вы всегда добавляете в префикс требования к методу типа, staticкогда они определены в протоколе. Это верно, даже если требования к методу типа начинаются с ключевого слова classor 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модификатора, потому что конечные классы не могут быть разделены на подклассы. 

Если подкласс переопределяет указанный инициализатор из суперкласса, а также реализует соответствующее требование инициализатора из протокола, пометьте реализацию инициализатора как модификаторами, так 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 Initializer .

Требуемое условие инициализации может быть удовлетворено неудачным или ненадежным инициализатором соответствующего типа. Требование об отказоустойчивом инициализаторе может быть выполнено с помощью отказавшего инициализатора или неявно развернутого отказавшего инициализатора.

Протоколы как типы

Протоколы на самом деле не реализуют никакой функциональности. Тем не менее, вы можете использовать протоколы как полноценные типы в вашем коде. Использование протокола в качестве типа иногда называют экзистенциальным типом , который происходит от фразы «существует тип T такой, что T соответствует протоколу».

Вы можете использовать протокол во многих местах, где разрешены другие типы, в том числе:

  • Как тип параметра или тип возвращаемого значения в функции, методе или инициализаторе
  • Как тип константы, переменной или свойства
  • Как тип элементов в массиве, словаре или другом контейнере

ЗАМЕТКА

Поскольку протоколы типа, начинают их имена с заглавной буквой (например , как FullyNamedи RandomNumberGenerator) , чтобы соответствовать именам других типов в Swift (например IntStringи 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способами, которые применимы ко всем генераторам, которые соответствуют этому протоколу. Это означает, что он не может использовать какие-либо методы или свойства, которые определены базовым типом генератора. Тем не менее, вы можете выполнить переход от типа протокола к базовому типу так же, как вы можете уменьшить класс от суперкласса до подкласса.

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классу позже в этой главе объявить, что его делегат должен использовать слабую ссылку. 

Вот версия игры Snakes and Ladders, изначально представленной в Control Flow . Эта версия адаптирована для использования 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)

}

}

Эта версия игры заключена в класс под названием SnakesAndLadders, который принимает DiceGameпротокол. Он предоставляет diceсвойство gettable и play()метод для соответствия протоколу. ( diceСвойство объявляется как постоянное свойство, потому что его не нужно менять после инициализации, а протокол требует, чтобы оно было только для получения.)

Настройка игрового поля Snakes and Ladders происходит в init()инициализаторе класса . Вся игровая логика перенесена в метод протокола play, который использует обязательное diceсвойство протокола для предоставления значений броска костей.

Обратите внимание, что delegateсвойство определено как необязательноеDiceGameDelegate , поскольку для игры не требуется делегат. Поскольку это необязательный тип, для delegateсвойства автоматически устанавливается начальное значение nil. После этого в игровом инстанциаторе есть возможность установить для свойства подходящий делегат. Поскольку DiceGameDelegateпротокол предназначен только для класса, вы можете объявить делегат weakдля предотвращения циклов ссылок.

DiceGameDelegateпредоставляет три метода для отслеживания хода игры. Эти три метода были включены в игровую логику в play()вышеописанном методе и вызываются, когда начинается новая игра, начинается новый ход или заканчивается игра.

Поскольку delegateсвойство является необязательным DiceGameDelegate , play()метод использует необязательную цепочку каждый раз, когда вызывает метод для делегата. Если delegateсвойство имеет значение nil, эти вызовы делегатов завершаются неудачно и без ошибок. Если delegateсвойство не ноль, методы делегата вызываются и передаются 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 предложение.

Следующее расширение заставляет 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"

ЗАМЕТКА

Типы не принимают протокол автоматически, просто удовлетворяя его требованиям. Они всегда должны прямо заявлять о своем принятии протокола.

Коллекции типов протоколов

Протокол может использоваться как тип, который будет храниться в коллекции, такой как массив или словарь. Этот пример создает массив 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, которое называется prettyTextualDescriptionString.

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протокола, чтобы начать строку вывода. Он добавляет двоеточие и разрыв строки и использует это как начало своего симпатичного текстового представления. Затем он перебирает массив квадратов доски и добавляет геометрическую форму для представления содержимого каждого квадрата:

  • Если значение квадрата больше чем 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 nameAgedПротокол имеет единственное требование для GetTable Intсобственности называется age. Оба протокола приняты структурой под названием Person.

В примере также определяется wishHappyBirthday(to:)функция. Тип celebratorпараметра is , что означает «любой тип, который соответствует как протоколам, так и протоколам». Не имеет значения, какой конкретный тип передается в функцию, если он соответствует обоим требуемым протоколам.Named & AgedNamedAged

Затем пример создает новый 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 & NamedLocationNamedCity

Передача birthdayPersonв beginConcert(in:)функцию недопустима, потому что Personне является подклассом Location. Аналогично, если вы создали подкласс, Locationкоторый не соответствует Namedпротоколу, вызов beginConcert(in:)с экземпляром этого типа также недопустим.

Проверка соответствия протокола

Вы можете использовать isи asоператор , описанные в Type Casting для проверки соответствия протокола, и приведение к определенному протоколу. Проверка и приведение к протоколу происходит точно так же, как и проверка и приведение к типу:

  • 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 }

}

В CircleCountryи 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свойству можно обращаться и печатать безопасным для типов способом.

Обратите внимание, что базовые объекты не изменяются в процессе приведения. Они продолжают быть Circle, а Countryи ан 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Класс сохраняет свое текущее значение свойства переменного называется countCounterКласс также определяет метод , называемый 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с использованием необязательного связывания. Если необязательный параметр Intсодержит значение, т. Е. Если и делегат, и метод существуют, а метод возвратил значение, развернутый объект amountдобавляется в сохраненное countсвойство, и приращение завершается.

Если это не возможно , чтобы получить значение из increment(forCount:)метода, либо потому , что dataSourceотсутствует, либо потому , что источник данных не реализует 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предложение. 

Например, вы можете определить расширение 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и целые числа соответствуют EquatableequalNumbersи differentNumbersмогут использовать allEqual()метод:

print(equalNumbers.allEqual())

// Prints "true"

print(differentNumbers.allEqual())

// Prints "false"

ЗАМЕТКА

Если соответствующий тип удовлетворяет требованиям для нескольких расширений с ограничениями, которые предоставляют реализации для одного и того же метода или свойства, Swift использует реализацию, соответствующую наиболее специализированным ограничениям.