Протокол определяет план методов, свойств и других требований , которые подходят для конкретной задачи или часть функциональности. Затем протокол может быть принят классом, структурой или перечислением, чтобы обеспечить фактическую реализацию этих требований. Говорят, что любой тип, который удовлетворяет требованиям протокола, соответствует этому протоколу.
Помимо указания требований, которые должны реализовывать соответствующие типы, вы можете расширить протокол для реализации некоторых из этих требований или для реализации дополнительных функций, которыми могут воспользоваться соответствующие типы.
Синтаксис протокола
Вы определяете протоколы очень похоже на классы, структуры и перечисления:
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
определяйте префиксные требования к свойствам типа с ключевым словом, когда вы определяете их в протоколе. Это правило относится, даже если требования к свойствам типа могут быть добавлены с ключевым словом class
or static
при реализации классом:
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
Вот пример протокола с требованием свойства единственного экземпляра:
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed
Протокол требует типа конформного , чтобы обеспечить полное имя. Протокол не определяет ничего другого о природе соответствующего типа - он только указывает, что тип должен иметь возможность предоставить полное имя для себя. Протокол гласит, что любой FullyNamed
тип должен иметь вызываемое свойство экземпляра gettable fullName
, которое имеет тип String
.
Вот пример простой структуры, которая принимает и соответствует FullyNamed
протоколу:
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"
В этом примере определяется структура с именем Person
, которая представляет конкретную именованную личность. В нем говорится, что он принимает FullyNamed
протокол как часть первой строки своего определения.
Каждый экземпляр Person
имеет одно сохраненное свойство с именем fullName
, которое имеет тип String
. Это соответствует единственному требованию FullyNamed
протокола и означает, что Person
оно правильно соответствует протоколу. (Swift сообщает об ошибке во время компиляции, если требование протокола не выполнено.)
Вот более сложный класс, который также принимает и соответствует FullyNamed
протоколу:
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
return (prefix != nil ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"
Этот класс реализует fullName
требование к свойству как вычисляемое свойство только для чтения для звездолета. Каждый Starship
экземпляр класса хранит обязательный name
и необязательный prefix
. fullName
Свойство использует prefix
значение , если оно существует, и присоединяет его к началу , name
чтобы создать полное имя для звездолета.
Требования к методу
Протоколы могут требовать реализации конкретных методов экземпляров и методов типов с помощью соответствующих типов. Эти методы пишутся как часть определения протокола точно так же, как для обычных методов экземпляра и типа, но без фигурных скобок или тела метода. Допускаются переменные параметры, подчиняющиеся тем же правилам, что и для обычных методов. Однако значения по умолчанию не могут быть указаны для параметров метода в определении протокола.
Как и в случае с требованиями к свойствам типа, вы всегда добавляете в префикс требования к методу типа, static
когда они определены в протоколе. Это верно, даже если требования к методу типа начинаются с ключевого слова class
or static
при реализации классом:
protocol SomeProtocol {
static func someTypeMethod()
}
В следующем примере определяется протокол с требованием метода одного экземпляра:
protocol RandomNumberGenerator {
func random() -> Double
}
Этот протокол RandomNumberGenerator
требует, чтобы у любого соответствующего типа был вызван метод экземпляра random
, который возвращает Double
значение всякий раз, когда он вызывается. Хотя это не указано как часть протокола, предполагается, что это значение будет числом от 0.0
до (но не включая) 1.0
.
RandomNumberGenerator
Протокол не делает никаких предположений о том , как будет каждый случайное число, он просто требует генератор , чтобы обеспечить стандартный способ создания нового случайного числа.
Вот реализация класса, который принимает и соответствует RandomNumberGenerator
протоколу. Этот класс реализует алгоритм генератора псевдослучайных чисел, известный как линейный конгруэнтный генератор :
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0
let m = 139968.0
let a = 3877.0
let c = 29573.0
func random() -> Double {
lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
return lastRandom / m
}
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"
Требования метода мутации
Иногда необходимо, чтобы метод изменил (или изменил ) экземпляр, к которому он принадлежит. Например, методы экземпляров для типов значений (то есть структур и перечислений) помещают mutating
ключевое слово перед ключевым словом метода, func
чтобы указать, что этому методу разрешено изменять экземпляр, которому он принадлежит, и любые свойства этого экземпляра.
Если вы определяете требование метода экземпляра протокола, предназначенное для изменения экземпляров любого типа, который принимает протокол, пометьте метод mutating
ключевым словом как часть определения протокола. Это позволяет структурам и перечислениям принимать протокол и удовлетворять требованиям этого метода.
ЗАМЕТКА
Если вы помечаете требование метода экземпляра протокола как
mutating
, вам не нужно писатьmutating
ключевое слово при написании реализации этого метода для класса.mutating
Ключевое слово используется только структур и перечислений.
В приведенном ниже примере определяется вызываемый протокол Togglable
, который определяет требование метода одного экземпляра toggle
. Как следует из названия, toggle()
метод предназначен для переключения или инвертирования состояния любого соответствующего типа, обычно путем изменения свойства этого типа.
toggle()
Метод помечается mutating
ключевым словом , как часть Togglable
определения протокола, чтобы указать , что этот метод , как ожидается , мутировать состояние соответствующей инстанции , если это называется:
protocol Togglable {
mutating func toggle()
}
Если вы реализуете Togglable
протокол для структуры или перечисления, эта структура или перечисление могут соответствовать протоколу, предоставляя реализацию toggle()
метода, также помеченного как mutating
.
В приведенном ниже примере определяется перечисление OnOffSwitch
. Это перечисление переключается между двумя состояниями, указанными в случаях перечисления on
и off
. Реализация перечисления toggle
помечена как mutating
, чтобы соответствовать требованиям Togglable
протокола:
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off:
self = .on
case .on:
self = .off
}
}
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on
Требования к инициализатору
Протоколы могут требовать реализации определенных инициализаторов с помощью соответствующих типов. Вы пишете эти инициализаторы как часть определения протокола точно так же, как для обычных инициализаторов, но без фигурных скобок или тела инициализатора:
protocol SomeProtocol {
init(someParameter: Int)
}
Реализации класса требований инициализатора протокола
Вы можете реализовать требование инициализатора протокола в соответствующем классе как назначенный инициализатор или удобный инициализатор. В обоих случаях вы должны пометить реализацию инициализатора required
модификатором:
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
Использование required
модификатора гарантирует, что вы предоставите явную или унаследованную реализацию требования инициализатора для всех подклассов соответствующего класса, чтобы они также соответствовали протоколу.
ЗАМЕТКА
Вам не нужно отмечать реализации инициализатора протокола с помощью
required
модификатора на классах, помеченных с помощьюfinal
модификатора, потому что конечные классы не могут быть разделены на подклассы.
Если подкласс переопределяет указанный инициализатор из суперкласса, а также реализует соответствующее требование инициализатора из протокола, пометьте реализацию инициализатора как модификаторами, так 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 (напримерInt
,String
иDouble
).
Вот пример протокола, используемого в качестве типа:
class Dice {
let sides: Int
let generator: RandomNumberGenerator
init(sides: Int, generator: RandomNumberGenerator) {
self.sides = sides
self.generator = generator
}
func roll() -> Int {
return Int(generator.random() * Double(sides)) + 1
}
}
В этом примере определяется новый класс под названием Dice
, который представляет собой n- стороннюю кость для использования в настольной игре. Dice
экземпляры имеют целочисленное свойство с именем sides
, которое представляет количество сторон, и вызываемое свойство generator
, которое обеспечивает генератор случайных чисел, из которого можно создавать значения броска костей.
generator
Свойство типа RandomNumberGenerator
. Следовательно, вы можете установить для него экземпляр любого типа, который принимает RandomNumberGenerator
протокол. Больше ничего не требуется от экземпляра, который вы назначаете этому свойству, кроме того, что экземпляр должен принять RandomNumberGenerator
протокол. Поскольку его тип таков RandomNumberGenerator
, код внутри Dice
класса может взаимодействовать только теми generator
способами, которые применимы ко всем генераторам, которые соответствуют этому протоколу. Это означает, что он не может использовать какие-либо методы или свойства, которые определены базовым типом генератора. Тем не менее, вы можете выполнить переход от типа протокола к базовому типу так же, как вы можете уменьшить класс от суперкласса до подкласса.
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, которое называется prettyTextualDescription
a String
.
SnakesAndLadders
Класс может быть расширен , чтобы принять и соответствовать PrettyTextRepresentable
:
extension SnakesAndLadders: PrettyTextRepresentable {
var prettyTextualDescription: String {
var output = textualDescription + ":\n"
for index in 1...finalSquare {
switch board[index] {
case let ladder where ladder > 0:
output += "▲ "
case let snake where snake < 0:
output += "▼ "
default:
output += "○ "
}
}
return output
}
}
Это расширение заявляет, что оно принимает PrettyTextRepresentable
протокол и обеспечивает реализацию prettyTextualDescription
свойства для SnakesAndLadders
типа. Все, что есть, PrettyTextRepresentable
также должно быть TextRepresentable
, и поэтому реализация prettyTextualDescription
начинается с обращения к textualDescription
свойству из TextRepresentable
протокола, чтобы начать строку вывода. Он добавляет двоеточие и разрыв строки и использует это как начало своего симпатичного текстового представления. Затем он перебирает массив квадратов доски и добавляет геометрическую форму для представления содержимого каждого квадрата:
- Если значение квадрата больше чем
0
, это основание лестницы и представлено как▲
. - Если значение квадрата меньше чем
0
, это голова змеи, и представлена▼
. - В противном случае значение квадрата равно
0
«свободному» квадрату, представленному○
.
prettyTextualDescription
Недвижимость теперь можно использовать для печати довольно текстового описания любого SnakesAndLadders
экземпляра:
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
Протоколы только для класса
Вы можете ограничить принятие протокола типами классов (а не структурами или перечислениями), добавив AnyObject
протокол в список наследования протокола.
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// class-only protocol definition goes here
}
В приведенном выше примере SomeClassOnlyProtocol
могут быть приняты только типы классов. Это ошибка времени компиляции, чтобы написать определение структуры или перечисления, которое пытается принять SomeClassOnlyProtocol
.
ЗАМЕТКА
Используйте протокол только для класса, когда поведение, определенное требованиями этого протокола, предполагает или требует, чтобы соответствующий тип имел ссылочную семантику, а не семантику значения.
Состав протокола
Может быть полезно требовать, чтобы тип соответствовал нескольким протоколам одновременно. Вы можете объединить несколько протоколов в одно требование с составом протокола . Составы протоколов ведут себя так, как будто вы определили временный локальный протокол, который объединяет требования всех протоколов в составе. Составы протоколов не определяют никаких новых типов протоколов.
Протокол составов имеет вид . Вы можете перечислить столько протоколов, сколько вам нужно, разделяя их амперсандами ( ). В дополнение к списку протоколов состав протокола может также содержать один тип класса, который можно использовать для указания необходимого суперкласса.SomeProtocol & AnotherProtocol
&
Вот пример, который объединяет два вызванных протокола Named
и Aged
в одно требование к составлению протокола для параметра функции:
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"
В этом примере Named
протокол имеет единственное требование для String
вызываемого свойства gettable name
. Aged
Протокол имеет единственное требование для GetTable Int
собственности называется age
. Оба протокола приняты структурой под названием Person
.
В примере также определяется wishHappyBirthday(to:)
функция. Тип celebrator
параметра is , что означает «любой тип, который соответствует как протоколам, так и протоколам». Не имеет значения, какой конкретный тип передается в функцию, если он соответствует обоим требуемым протоколам.Named & Aged
Named
Aged
Затем пример создает новый Person
вызванный экземпляр birthdayPerson
и передает этот новый экземпляр wishHappyBirthday(to:)
функции. Поскольку Person
этот протокол соответствует обоим протоколам, этот вызов действителен, и wishHappyBirthday(to:)
функция может распечатать свое поздравление с днем рождения.
Вот пример, который объединяет Named
протокол из предыдущего примера с Location
классом:
class Location {
var latitude: Double
var longitude: Double
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}
class City: Location, Named {
var name: String
init(name: String, latitude: Double, longitude: Double) {
self.name = name
super.init(latitude: latitude, longitude: longitude)
}
}
func beginConcert(in location: Location & Named) {
print("Hello, \(location.name)!")
}
let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"
beginConcert(in:)
Функция принимает параметр типа , что означает «любой тип , что это подкласс и который соответствует протоколу.» В этом случае, удовлетворяет обоим требованиям.Location & Named
Location
Named
City
Передача birthdayPerson
в beginConcert(in:)
функцию недопустима, потому что Person
не является подклассом Location
. Аналогично, если вы создали подкласс, Location
который не соответствует Named
протоколу, вызов beginConcert(in:)
с экземпляром этого типа также недопустим.
Проверка соответствия протокола
Вы можете использовать is
и as
оператор , описанные в 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 }
}
В Circle
, Country
и Animal
классы не имеют общий базовый класс. Тем не менее, все они являются классами, и поэтому экземпляры всех трех типов могут использоваться для инициализации массива, в котором хранятся значения типа AnyObject
:
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
objects
Массив инициализируется с массивом буквальным , содержащего Circle
экземпляр с радиусом 2 единицы; Country
экземпляр инициализируется с площадью поверхности Соединенного Королевства в квадратных километрах; и Animal
экземпляр с четырьмя ногами.
Теперь objects
массив можно повторять, и каждый объект в массиве можно проверить, чтобы убедиться, что он соответствует HasArea
протоколу:
for object in objects {
if let objectWithArea = object as? HasArea {
print("Area is \(objectWithArea.area)")
} else {
print("Something that doesn't have an area")
}
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area
Всякий раз, когда объект в массиве соответствует HasArea
протоколу, необязательное значение, возвращаемое as?
оператором, разворачивается с необязательным связыванием в вызываемую константу objectWithArea
. Известно, что objectWithArea
константа имеет тип HasArea
, поэтому к ее area
свойству можно обращаться и печатать безопасным для типов способом.
Обратите внимание, что базовые объекты не изменяются в процессе приведения. Они продолжают быть 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
Класс сохраняет свое текущее значение свойства переменного называется count
. Counter
Класс также определяет метод , называемый increment
, который увеличивает значение count
свойства каждый раз , когда метод вызывается.
increment()
Метод сначала пытается получить сумму приращения, смотря для реализации increment(forCount:)
метода на его источнике данных. increment()
Метод использует дополнительные цепочки , чтобы попытаться вызвать increment(forCount:)
, и передает текущее count
значение в качестве единственного аргумента метода.
Обратите внимание, что здесь доступны два уровня необязательной цепочки. Во-первых, возможно, что dataSource
может быть nil
, и поэтому dataSource
после его имени стоит вопросительный знак, указывающий, что его increment(forCount:)
следует вызывать, только если dataSource
это не так nil
. Во-вторых, даже если dataSource
онсуществует, нет гарантии, что он реализуется increment(forCount:)
, потому что это необязательное требование. Здесь возможность, которая increment(forCount:)
может не быть реализована, также обрабатывается необязательной цепочкой. Призыв increment(forCount:)
происходит, только если increment(forCount:)
существует, то есть, если это не так nil
. Вот почему increment(forCount:)
также написано с вопросительным знаком после его имени.
Поскольку вызов increment(forCount:)
может завершиться неудачей по любой из этих двух причин, вызов возвращает необязательное Int
значение. Это верно, хотя increment(forCount:)
определяется как возвращение не необязательного Int
значения в определении CounterDataSource
. Несмотря на то, что есть две необязательные операции связывания, одна за другой, результат по-прежнему заключен в одну необязательную операцию.
После вызова возвращаемое increment(forCount:)
необязательное значение Int
разворачивается в вызываемую константу amount
с использованием необязательного связывания. Если необязательный параметр 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
и целые числа соответствуют Equatable
, equalNumbers
и differentNumbers
могут использовать allEqual()
метод:
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"
ЗАМЕТКА
Если соответствующий тип удовлетворяет требованиям для нескольких расширений с ограничениями, которые предоставляют реализации для одного и того же метода или свойства, Swift использует реализацию, соответствующую наиболее специализированным ограничениям.
0 комментариев