Spec-Zone.ru › Swift Language

Протоколы

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

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

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

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

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

protocol SomeProtocol {
    // protocol definition goes here
}

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

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

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

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

Примечание: поскольку протоколы являются типами, начинайте их имена с большой буквы (например, FullyNamed и RandomNumberGenerator), чтобы соответствовать именам других типов в Swift (например, Int, String и Double).

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

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

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

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

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

Всегда используйте префикс ключевого слова static для требований к свойствам типа, когда вы определяете их в протоколе. Это правило относится даже тогда, когда требования к свойствам типа могут иметь префиксы class или static, когда они реализованы классом:

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

Вот пример протокола с одним требованием к свойству экземпляра:

protocol FullyNamed {
    var fullName: String { get }
}

Протокол FullyNamed требует, чтобы соответствующий тип предоставлял полное имя. Протокол не уточняет ничего о природе соответствующего типа — он только указывает, что тип должен иметь возможность предоставить для себя полное имя. Протокол утверждает, что любой тип FullyNamed должен иметь свойство экземпляра для чтения, называемое 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 или 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 метода, чтобы указать, что метод может изменять экземпляр, к которому он принадлежит, и любые свойства этого экземпляра. Этот процесс описан в <doc:Methods#Modifying-Value-Types-from-Within-Instance-Methods>.

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

Для получения дополнительной информации о требуемых инициализаторах см. <doc:Initialization#Required-Initializers>.

Примечание: вам не нужно отмечать реализации инициализаторов протокола модификатором required в классах, помеченных модификатором final, поскольку финальные классы не могут быть подклассами. Дополнительные сведения о модификаторе final см. в <doc:Inheritance#Preventing-Overrides>.

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

Требования к инициализаторам с возможностью неудачи

Протоколы могут определять требования к инициализаторам с возможностью неудачи для соответствующих типов, как определено в <doc:Initialization#Failable-Initializers>.

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

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

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

END_OF_DOCUMENT_MARKER

Наиболее распространенный способ использования протокола в качестве типа — это использование протокола в качестве обобщенного ограничения. Код с обобщенными ограничениями может работать с любым типом, который соответствует протоколу, а конкретный тип выбирается кодом, использующим API. Например, при вызове функции, которая принимает аргумент, а тип этого аргумента обобщенный, вызывающий выбирает тип.

Код с непрозрачным типом работает с каким-то типом, который соответствует протоколу. Базовый тип известен во время компиляции, и реализация API выбирает этот тип, но идентичность этого типа скрыта от клиентов API. Использование непрозрачного типа позволяет предотвратить утечку подробностей реализации API через уровень абстракции — например, скрывая конкретный возвращаемый тип функции и гарантируя только, что значение соответствует заданному протоколу.

Код с упакованным типом протокола работает с любым типом, выбранным во время выполнения, который соответствует протоколу. Для поддержки этой гибкости во время выполнения Swift добавляет уровень косвенности при необходимости — так называемый ящик, что имеет затраты на производительность. Из-за этой гибкости Swift не знает базовый тип во время компиляции, что означает, что вы можете получить доступ только к членам, которые требуются протоколом. Для доступа к другим API базового типа требуется приведение типов во время выполнения.

Дополнительную информацию об использовании протоколов в качестве обобщенных ограничений см. в Обобщенные типы. Дополнительную информацию о непрозрачных типах и упакованных типах протоколов см. в НепрозрачныеТипы.

Делегирование

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

В примере ниже определена игра в кости и вложенный протокол для делегата, который отслеживает ход игры:

class DiceGame {
    let sides: Int
    let generator = LinearCongruentialGenerator()
    weak var delegate: Delegate?

    init(sides: Int) {
        self.sides = sides
    }

    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }

    func play(rounds: Int) {
        delegate?.gameDidStart(self)
        for round in 1...rounds {
            let player1 = roll()
            let player2 = roll()
            if player1 == player2 {
                delegate?.game(self, didEndRound: round, winner: nil)
            } else if player1 > player2 {
                delegate?.game(self, didEndRound: round, winner: 1)
            } else {
                delegate?.game(self, didEndRound: round, winner: 2)
            }
        }
        delegate?.gameDidEnd(self)
    }

    protocol Delegate: AnyObject {
        func gameDidStart(_ game: DiceGame)
        func game(_ game: DiceGame, didEndRound round: Int, winner: Int?)
        func gameDidEnd(_ game: DiceGame)
    }
}

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

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

Для предотвращения сильных циклов ссылок делегаты объявляются как слабые ссылки. Дополнительную информацию о слабых ссылках см. в <doc:AutomaticReferenceCounting#Strong-Reference-Cycles-Between-Class-Instances>. Отметка протокола как только для классов позволяет классу DiceGame объявить, что его делегат должен использовать слабую ссылку. Протокол, предназначенный только для классов, отмечается наследованием от AnyObject, как обсуждалось в <doc:Protocols#Class-Only-Protocols>.

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

Поскольку свойство delegate является необязательным DiceGame.Delegate, метод play(rounds:) использует цепочку опциональности каждый раз, когда он вызывает метод делегата, как обсуждалось в ЦепочкаОпциональности. Если свойство delegate имеет значение nil, эти вызовы делегата игнорируются. Если свойство delegate имеет ненулевое значение, методы делегата вызываются и передают экземпляр DiceGame в качестве параметра.

Следующий пример демонстрирует класс под названием DiceGameTracker, который принимает протокол DiceGame.Delegate:

class DiceGameTracker: DiceGame.Delegate {
    var playerScore1 = 0
    var playerScore2 = 0
    func gameDidStart(_ game: DiceGame) {
        print("Started a new game")
        playerScore1 = 0
        playerScore2 = 0
    }
    func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) {
        switch winner {
            case 1:
                playerScore1 += 1
                print("Player 1 won round \(round)")
            case 2: playerScore2 += 1
                print("Player 2 won round \(round)")
            default:
                print("The round was a draw")
        }
    }
    func gameDidEnd(_ game: DiceGame) {
        if playerScore1 == playerScore2 {
            print("The game ended in a draw.")
        } else if playerScore1 > playerScore2 {
            print("Player 1 won!")
        } else {
            print("Player 2 won!")
        }
    }
}

Класс DiceGameTracker реализует все три метода, требуемые протоколом DiceGame.Delegate. Он использует эти методы для обнуления очков обоих игроков в начале новой игры, для обновления их очков в конце каждого раунда и для объявления победителя в конце игры.

Вот как DiceGame и DiceGameTracker выглядят в действии:

let tracker = DiceGameTracker()
let game = DiceGame(sides: 6)
game.delegate = tracker
game.play(rounds: 3)
// Started a new game
// Player 2 won round 1
// Player 2 won round 2
// Player 1 won round 3
// Player 2 won!

Добавление соответствия протоколу с помощью расширения

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

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

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

protocol TextRepresentable {
    var textualDescription: String { get }
}

Класс Dice из приведенного выше примера может быть расширен, чтобы принять и соответствовать TextRepresentable:

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

Это расширение принимает новый протокол точно так же, как если бы Dice предоставил его в своей исходной реализации. Название протокола указывается после имени типа, разделенных двоеточием, и реализация всех требований протокола предоставляется в фигурных скобках расширения.

Теперь любой экземпляр Dice может рассматриваться как TextRepresentable:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"

Аналогично, класс игры SnakesAndLadders может быть расширен, чтобы принять и соответствовать протоколу TextRepresentable:

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)
// Prints "A game of Snakes and Ladders with 25 squares"

Условное соответствие протоколу

Обобщенный тип может удовлетворять требованиям протокола только при определенных условиях, например, когда обобщенный параметр типа соответствует протоколу. Вы можете сделать обобщенный тип условно соответствующим протоколу, указав ограничения при расширении типа. Запишите эти ограничения после имени протокола, который вы принимаете, написав обобщенный where. Дополнительные сведения об обобщенных where, см. в <doc:Generics#Generic-Where-Clauses>.

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

Примечание: типы не принимают протокол автоматически только по удовлетворению его требований. Они всегда должны явно объявлять свое принятие протокола.

Принятие протокола с использованием сгенерированной реализации

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

Swift предоставляет сгенерированную реализацию Equatable для следующих типов пользовательских типов:

  • Структуры, которые содержат только хранимые свойства, соответствующие протоколу Equatable
  • Перечисления, которые содержат только связанные типы, соответствующие протоколу Equatable
  • Перечисления, которые не имеют связанных типов

Чтобы получить сгенерированную реализацию ==, объявите соответствие протоколу Equatable в файле, содержащем исходное объявление, не реализовывая оператор == самостоятельно. Протокол Equatable предоставляет реализацию по умолчанию для !=.

В примере ниже определена структура Vector3D для вектора трехмерной позиции (x, y, z), аналогично структуре Vector2D. Поскольку свойства x, y и z имеют тип Equatable, Vector3D получает сгенерированные реализации операторов эквивалентности.

struct Vector3D: Equatable {
    var x = 0.0, y = 0.0, z = 0.0
}

let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
if twoThreeFour == anotherTwoThreeFour {
    print("These two vectors are also equivalent.")
}
// Prints "These two vectors are also equivalent."

Swift предоставляет сгенерированную реализацию Hashable для следующих типов пользовательских типов:

  • Структуры, которые содержат только хранимые свойства, соответствующие протоколу Hashable
  • Перечисления, которые содержат только связанные типы, соответствующие протоколу Hashable
  • Перечисления, которые не имеют связанных типов

Чтобы получить сгенерированную реализацию hash(into:), объявите соответствие протоколу Hashable в файле, содержащем исходное объявление, не реализовывая метод hash(into:) самостоятельно.

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

В примере ниже определено перечисление SkillLevel с случаями для новичков, промежуточных пользователей и экспертов. Эксперты дополнительно ранжируются по количеству звезд.

enum SkillLevel: Comparable {
    case beginner
    case intermediate
    case expert(stars: Int)
}
var levels = [SkillLevel.intermediate, SkillLevel.beginner,
              SkillLevel.expert(stars: 5), SkillLevel.expert(stars: 3)]
for level in levels.sorted() {
    print(level)
}
// Prints "beginner"
// Prints "intermediate"
// Prints "expert(stars: 3)"
// Prints "expert(stars: 5)"

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

Протокол может использоваться в качестве типа, который хранится в коллекции, такой как массив или словарь, как упоминалось в <doc:Protocols#Protocols-as-Types>. В этом примере создается массив элементов TextRepresentable:

let things: [TextRepresentable] = [game, d12, simonTheHamster]

Теперь можно перебирать элементы массива и печатать текстовое описание каждого элемента:

for thing in things {
    print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

Обратите внимание, что константа thing имеет тип TextRepresentable. Она не имеет типа Dice, DiceGame или Hamster, даже если фактический экземпляр за кулисами принадлежит одному из этих типов. Тем не менее, поскольку она имеет тип TextRepresentable, и известно, что всё, что имеет тип TextRepresentable, имеет свойство textualDescription, безопасно обращаться к thing.textualDescription в каждом цикле.

Наследование протоколов

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

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}

Вот пример протокола, который наследует протокол TextRepresentable из приведённого выше примера:

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

В этом примере определён новый протокол PrettyTextRepresentable, который наследует от TextRepresentable. Всё, что соответствует протоколу PrettyTextRepresentable, должно удовлетворять всем требованиям, навязанным протоколом TextRepresentable, плюс дополнительные требования, навязанные протоколом PrettyTextRepresentable. В этом примере PrettyTextRepresentable добавляет единственное требование предоставлять свойство для получения значения prettyTextualDescription, возвращающее значение типа String.

Класс SnakesAndLadders может быть расширен для принятия и соответствия протоколу PrettyTextRepresentable:

extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\n"
        for index in 1...finalSquare {
            switch board[index] {
            case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}

Это расширение указывает, что оно принимает протокол PrettyTextRepresentable и предоставляет реализацию свойства prettyTextualDescription для типа SnakesAndLadders. Всё, что соответствует протоколу PrettyTextRepresentable, должно также соответствовать протоколу TextRepresentable, поэтому реализация prettyTextualDescription начинается с доступа к свойству textualDescription из протокола TextRepresentable для начала строки вывода. Она добавляет двоеточие и перевод строки, используя это в качестве начала красивого текстового представления. Затем она итерируется по массиву игровых полей, добавляя геометрические фигуры, представляющие содержимое каждого поля:

  • Если значение поля больше, чем 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, будет ошибкой на этапе компиляции.

Примечание: Используйте протокол, доступный только для классов, когда поведение, определённое требованиями этого протокола, предполагает или требует, чтобы соответствующий тип имел семантику ссылок, а не семантику значений. Для получения дополнительной информации о семантике ссылок и значений см. <doc:ClassesAndStructures#Structures-and-Enumerations-Are-Value-Types> и <doc:ClassesAndStructures#Classes-Are-Reference-Types>.

Состав протоколов

Полезно потребовать, чтобы тип соответствовал нескольким протоколам одновременно. Вы можете объединить несколько протоколов в одно требование с помощью состава протоколов. Составы протоколов ведут себя так, как будто вы определили временный локальный протокол, который имеет объединённые требования всех протоколов в составе. Составы протоколов не определяют новых типов протоколов.

Составы протоколов имеют вид SomeProtocol & AnotherProtocol. Вы можете перечислить любое количество протоколов, разделяя их амперсандом (&). В дополнение к списку протоколов состав протоколов также может содержать один тип класса, который вы можете использовать для указания требуемого суперкласса.

Вот пример, который объединяет два протокола, называемые Named и Aged, в единственное требование состава протоколов для параметра функции:

protocol Named {
    var name: String { get }
}
protocol Aged {
    var age: Int { get }
}
struct Person: Named, Aged {
    var name: String
    var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"

В этом примере протокол Named имеет единственное требование для свойства для получения значения String, называемого name. Протокол Aged имеет единственное требование для свойства для получения значения Int, называемого age. Оба протокола принимаются структурой под названием Person.

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

Затем пример создаёт новый экземпляр Person под названием birthdayPerson и передаёт этот экземпляр в функцию wishHappyBirthday(to:). Поскольку Person соответствует обоим протоколам, этот вызов допустим, и функция wishHappyBirthday(to:) может вывести поздравление с днём рождения.

Вот пример, который объединяет протокол Named из предыдущего примера с классом Location:

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"

Функция beginConcert(in:) принимает параметр типа Location & Named, что означает «любой тип, который является подклассом Location и соответствует протоколу Named». В этом случае City удовлетворяет обоим требованиям.

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

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

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

  • Оператор is возвращает true, если экземпляр соответствует протоколу, и возвращает false, если не соответствует.
  • Версия оператора приведения типа as? возвращает необязательное значение типа протокола, и это значение является nil, если экземпляр не соответствует этому протоколу.
  • Версия оператора приведения типа as! принудительно приводит к типу протокола и вызывает ошибку времени выполнения, если приведение не выполняется.

В этом примере определён протокол под названием HasArea с единственным требованием свойства для получения значения Double свойства, называемого area:

protocol HasArea {
    var area: Double { get }
}

Вот два класса Circle и Country, оба из которых соответствуют протоколу HasArea:

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

Класс Circle реализует требование свойства area как вычисляемое свойство на основе хранимого свойства radius. Класс Country реализует требование area непосредственно как хранимое свойство. Оба класса правильно соответствуют протоколу HasArea.

Вот класс под названием Animal, который не соответствует протоколу HasArea:

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

Классы Circle, Country и Animal не имеют общего базового класса. Тем не менее, они все являются классами, и поэтому экземпляры всех трёх типов могут быть использованы для инициализации массива, хранящего значения типа AnyObject:

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

Массив objects инициализируется литералом массива, содержащим экземпляр Circle с радиусом 2 единицы; экземпляр Country, инициализированный площадью поверхности Соединённого Королевства в квадратных километрах; и экземпляр Animal с четырьмя ногами.

Массив objects теперь может быть проитерирован, и каждый объект в массиве может быть проверен на соответствие протоколу HasArea:

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

Всякий раз, когда объект в массиве соответствует протоколу HasArea, необязательное значение, возвращаемое оператором as?, распаковывается с помощью привязки к необязательным значениям в константу под названием objectWithArea. Константа objectWithArea известна как тип HasArea, поэтому к её свойству area можно обратиться и вывести его значение безопасным способом.

Обратите внимание, что исходные объекты не изменяются в процессе приведения. Они по-прежнему остаются Circle, Country и Animal. Однако в тот момент, когда они хранятся в константе objectWithArea, они известны только как тип HasArea, и поэтому можно получить доступ только к свойству area.

Необязательные требования протоколов

Вы можете определить необязательные требования для протоколов. Эти требования не обязательно должны быть реализованы типами, которые соответствуют протоколу. Необязательные требования предваряются модификатором optional в определении протокола. Необязательные требования доступны для того, чтобы вы могли писать код, взаимодействующий с Objective-C. Как протокол, так и необязательное требование должны быть помечены атрибутом @objc. Обратите внимание, что @objc протоколы могут быть приняты только классами, а не структурами или перечислениями.

Когда вы используете метод или свойство в необязательном требовании, его тип автоматически становится необязательным. Например, метод типа (Int) -> String становится ((Int) -> String)?. Обратите внимание, что весь тип функции заключён в необязательный тип, а не возвращаемое значение метода.

Необязательное требование протокола можно вызвать с использованием цепочки необязательных вызовов, чтобы учесть возможность того, что требование не было реализовано типом, соответствующим протоколу. Вы проверяете реализацию необязательного метода, добавив знак вопроса после имени метода при его вызове, например, someOptionalMethod?(someArgument). Для получения информации о цепочке необязательных вызовов см. OptionalChaining.

В следующем примере определен класс для подсчета целых чисел, называемый 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. Несмотря на то, что есть две операции необязательного цепочечного присваивания друг за другом, результат всё равно заключён в одно необязательное значение. Для получения дополнительной информации об использовании нескольких операций необязательного цепочечного присваивания см. <doc:OptionalChaining#Linking-Multiple-Levels-of-Chaining>.

После вызова метода increment(forCount:) необязательное значение Int, которое он возвращает, распаковывается в константу под названием amount, используя необязательную привязку. Если необязательное значение Int содержит значение — то есть, если делегат и метод оба существуют, а метод вернул значение — распакованное значение amount добавляется к свойству count, и инкрементация завершена.

Если невозможно получить значение из метода increment(forCount:) — либо потому, что dataSource равно nil, либо потому, что источник данных не реализует метод increment(forCount:) — то метод increment() пытается получить значение из свойства fixedIncrement источника данных вместо этого. Свойство fixedIncrement также является необязательным требованием, поэтому его значение является необязательным значением Int, даже если fixedIncrement определен как свойство без необязательности типа Int в рамках определения протокола CounterDataSource.

Вот простой пример реализации CounterDataSource, где источник данных каждый раз возвращает постоянное значение 3 при запросе. Он делает это, реализуя необязательное требование свойства fixedIncrement:

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

Вы можете использовать экземпляр класса ThreeSource в качестве источника данных для нового экземпляра класса Counter:

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

Код выше создает новый экземпляр класса Counter; задаёт его источник данных в новый экземпляр класса ThreeSource; и вызывает метод increment() счётчика четыре раза. Как ожидалось, свойство count счётчика увеличивается на три каждый раз, когда вызывается метод increment().

Вот более сложный источник данных под названием 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 оператор. Дополнительную информацию об обобщённых where операторах см. в <doc:Generics#Generic-Where-Clauses>.

Например, вы можете определить расширение протокола Collection, которое применяется к любому набору, элементы которого соответствуют протоколу Equatable. Ограничив элементы набора протоколом Equatable, частью стандартной библиотеки Swift, вы можете использовать операторы == и != для проверки равенства и неравенства между двумя элементами.

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 использует реализацию, соответствующую самым специализированным ограничениям.

This source file is part of the Swift.org open source project
Copyright © 2014 - 2025 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API