Link Search Menu Expand Document

Протоколи

Протокол визначає список методів, властивостей, та інших вимог, котрі підходять для певної задачі чи частини функціональності. Класи, структури та перечислення можуть підпорядковуватись протоколам, надаючи реалізації до цих вимог. Якщо тип виконує всі вимоги певного протоколу, про нього кажуть, що тип підпорядковується протоколу.

Окрім визначення вимог, яким повинні відповідати підпорядковані типи, протоколи можуть бути розширеними для реалізації цих вимог, або для реалізації додаткової функціональності, котрою можуть скористатись підпорядковані типи.

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

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

protocol SomeProtocol {
    // тут йде оголошення протоколу
}

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

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // тут йде оголошення структури
}

Якщо клас має батьківський клас, то назва батьківського класу має йти перед усіма протоколами, до яких підпорядкований тип, відділяючись від них комою:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // тут йде оголошення класу
}

Вимоги властивостей

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

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

Вимоги властивостей завжди оголошуються так само, як і самі властивості, за допомогою ключового слова var. Вимоги властивості для читання й запису при цьому помічаються конструкцією { get set } після їх оголошення, вимоги властивості тільки для читання – конструкцією { get }.

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

Вимоги властивостей типу завжди помічаються в протоколі ключовим словом static. При цьому, якщо протоколу підпорядковується клас, такі вимоги задовольняються як властивостями типу (static), та і властивостями класу (class).

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 stepan = Person(fullName: "Степан Яблучко")
// stepan.fullName дорівнює "Степан Яблучко"

У даному прикладі визначено структуру на ім’я 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 дорівнює "USS Enterprise"

Цей клас реалізовує вимогу властивості fullName за допомогою властивості тільки для читання, що обчислюється. Кожен екземпляр Starship зберігає обов’язкове значення name та опціональне значення prefix. Властивість fullName використовує значення prefix, якщо воно присутнє, та додає його до name зліва, щоб створити повне ім’я космічного корабля.

Вимоги методів

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

Як і з вимогами властивостей, слід завжди позначати вимоги методів типів за допомогою ключового слова static. При цьому реалізації цих методів у класах можуть позначатись як ключовим словом static, так і ключовим словом class:

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("Ось випадкове число: \(generator.random())")
// Надрукує "Ось випадкове число: 0.37464991998171"
print("А ось іще одне: \(generator.random())")
// Надрукує "А ось іще одне: 0.729023776863283"

Вимоги мутуючих методів

У методах часом буває потрібно змінити (або мутувати) екземпляр, до якого належить даний метод. Методи екземплярів типів-значень (тобто структур та перечислень), що можуть змінювати свій екземпляр або будь-яку з його властивостей, повинні позначатись ключовим словом mutating перед ключовим словом func. Цей процес детально описаний у підрозділі Зміни типів-значень в методах екземплярів.

При визначенні у протоколі вимог методів екземплярів, котрі призначені для зміни цих екземплярів, слід позначати такі вимоги ключовим словом mutating в оголошенні протоколу. Це дозволяє структурам та перечисленням підпорядковуватись протоколу та задовольняти вимоги мутуючих методів.

Примітка

Якщо позначити вимогу методу екземпляру ключовим словом mutating, при реалізації даного методу в класі, підпорядкованому даному протоколу, вказувати це ключове слово не потрібно. Ключове слово mutating використовується тільки для структур та перечислень.

У прикладі нижче оголошено протокол на ім’я Togglable, котрий визначає єдину вимогу методу екземпляру на ім’я toggle. Метод toggle() призначений для інвертування стану підпорядкованого типу, тобто для перемикання стану на протилежний, шляхом зміни властивості цього типу.

В оголошенні протоколу Togglable вимогу методу toggle() позначено ключовим словом mutating, щоб відобразити, що метод toggle() призначений для зміни стану підпорядкованого екземпляру:

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 тепер дорівнює .on

Вимоги ініціалізаторів

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

protocol SomeProtocol {
    init(someParameter: Int)
}

Реалізація вимог ініціалізаторів у класах

Вимоги ініціалізаторів у класах можна реалізовувати як у вигляді призначених ініціалізаторів, так і у вигляді ініціалізаторів для зручності. В обох випадках, слід реалізацію ініціалізатору слід позначати ключовим словом required:

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // тут йде реалізація ініціалізатора
    }
}

Використання модифікатора required гарантує, що явну чи успадковану реалізацію ініціалізатора буде надано в усіх нащадках підпорядкованого класу, і таким чином вони також підпорядковуються до даного протоколу.

Детальніше з обов’язковими ініціалізаторами можна ознайомитись у підрозділі [Обов’язкові ініціалізатори](/1_language_guide/13_initialization.md#Обов’язкові-ініціалізатори).

Примітка

Для класів, позначених ключовим словом final, позначати реалізацію ініціалізатора протоколу не потрібно, оскільки фінальні класи не можна наслідувати. Детальніше з ключовим словом final можна ознайомитись у підрозділі Запобігання заміщенню.

Якщо клас-нащадок заміщує призначений ініціалізатор батьківського класу, і він також реалізовує вимогу ініціалізатора протоколу, реалізацію ініціалізатора в нащадку слід позначати одночасно ключовими словами required та override:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {        
        // тут йде реалізація ініціалізатора
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" через підпорядкованість до протоколу SomeProtocol; 
    // "override" через наслідування SomeSuperClass:
    required override init() {
        // тут йде реалізація ініціалізатора
    }
}

Вимоги ненадійних ініціалізаторів

Протоколи можуть визначати вимоги ненадійних ініціалізаторів, котрі описані у підрозділі Ненадійні ініціалізатори.

Вимогу ненадійного ініціалізатора можна задовольнити як ненадійним, так і звичайним, надійним ініціалізатором підпорядкованого типу. Вимогу надійного ініціалізатора можна задовольнити або надійним ініціалізатором, або ненадійним ініціалізатором init!.

Протоколи як типи

Протоколи самі по собі фактично не реалізовують жодної функціональності. Разом з тим, кожен протокол можна використовувати у коді як повноцінний тип.

Оскільки протокол є типом, його можна використовувати у багатьох місцях, де дозволяються інші типи, в тому числі:

  • Як тип параметра чи тип, що повертається у функціях, методах чи ініціалізаторах.
  • Як тип константи, змінної чи властивості
  • Як тип елементів масиву, словнику чи іншої колекції.

Примітка

Оскільки протоколи є типами, слід записувати їх назви з великої літери (як, наприклад, 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, котрий також має тип RandomNumberGenerator. Таким чином при ініціалізації нового екземпляру Dice можна передавати значення будь-якого підпорядкованого типу в якості цього параметра.

Клас Dice також має метод екземпляру, roll(), що повертає цілочисельне значення в проміжку від 1 до sides (кількості сторін гральної кості). Цей метод викликає метод random() властивості generator, котрий повертає випадкове число в діапазоні від 0.0 до 1.0, після чого це число використовується для отримання значення кості всередині її діапазону значень. Оскільки екземпляр generator є підпорядкованим протоколу RandomNumberGenerator, він гарантовано має метод random().

Ось приклад використання класу Dice для створення шестигранної гральної кості з екземпляром LinearCongruentialGenerator в якості її генератора випадкових чисел:

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("Випадковий кидок кості: \(d6.roll())")
}
// Випадковий кидок кості: 3
// Випадковий кидок кості: 5
// Випадковий кидок кості: 4
// Випадковий кидок кості: 5
// Випадковий кидок кості: 4

Делегування

Делегування – це шаблон проєктування, що дозволяє класу чи структурі передати (або делегувати) якісь зі своїх відповідальностей до екземпляра іншого типу. Цей шаблон проєктування реалізовується за допомогою протоколу, що інкапсулює делеговані відповідальності таким чином, що підпорядкований тип (котрий називають делегатом) гарантовано реалізовує делеговану функціональність. Делегування може використовуватись як відповідь на певну дію, чи для отримання даних із зовнішнього джерела, без необхідності знати фактичний тип цього джерела.

У прикладі нижче визначено два протоколи для використання у настільних іграх, де потрібні гральні кості:

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

Протоколу DiceGame може підпорядковуватись будь-яка гра, що передбачає кидання костей. Протоколу DiceGameDelegate може підпорядковуватись будь-який тип, якому потрібно відстежувати прогрес у грі, що представляє DiceGame.

Ось версія гри Ліла (або Змії та сходи), котру вперше було представлено у розділі Потік керування. Ця версія є адаптованою до використання екземпляру Dice для кидків костей, підпорядкованою до протоколу DiceGame, та повідомляє свій прогрес за допомогою протоколу DiceGameDelegate:

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    init() {
        board = Array(repeating: 0, count: finalSquare + 1)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    var delegate: DiceGameDelegate?
    func play() {
        square = 0
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
            switch square + diceRoll {
            case finalSquare:
                break gameLoop
            case let newSquare where newSquare > finalSquare:
                continue gameLoop
            default:
                square += diceRoll
                square += board[square]
            }
        }
        delegate?.gameDidEnd(self)
    }
}

Описання ігрового процесу гри Ліла можна знайти у секції Інструкція Break розділу Потік керування.

Ця версія гри є загорнутою в клас SnakesAndLadders, котрий реалізовує протокол DiceGame. Він має властивість для читання dice та метод play(), і таким чином підпорядковується до цього протоколу. (Властивість dice оголошено як константну властивість, оскільки вона не змінюється після ініціалізації, а протокол вимагає лише щоб цю властивість можна було прочитати).

Налаштування дошки в грі Ліла відбувається в ініціалізаторі класу init(). Вся ігрова логіка переїхала до методу реалізації методу протоколу play(), в котрій використовується властивість протоколу dice, для отримання значення кинутої кості.

Слід помітити, що властивість delegate визначено як опціональний DiceGameDelegate, оскільки для гри не обов’язково мати делегат. Оскільки властивість delegate має опціональний тип, вона автоматично отримує початкове значення nil. Після цього, той, хто створив гру, має можливість присвоїти властивості delegate відповідне значення.

Протокол DiceGameDelegate має три методи для відслідковування прогресу гри. Ці методи було інкорпоровано в ігрову логіку всередині методу play() вище; вони викликаються на початку гри, на початку ходу та в кінці гри.

Оскільки властивість delegate є опціональним DiceGameDelegate, метод play() викликає методи делегату за допомогою ланцюжку опціоналів. Якщо властивість delegate має значення nil, ці звернення до делегата будуть проігноровані, без помилок. Якщо властивість delegate має значення і воно не nil, будуть викликані методи делегату, і до них буде передано екземпляр SnakesAndLadders в якості параметра.

У наступному прикладі показано клас на ім’я DiceGameTracker, котрий підпорядковується до протоколу DiceGameDelegate:

class DiceGameTracker: DiceGameDelegate {
    var numberOfTurns = 0
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        if game is SnakesAndLadders {
            print("Розпочалась нова гра у Змії та Сходи")
        }
        print("У грі використовуються \(game.dice.sides)-сторонні гральні кості")
    }
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("На костях випало \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("Гра тривала \(numberOfTurns) ходи(ів)")
    }
}

Клас DiceGameTracker реалізовує всі три методи, що вимагаються протоколом DiceGameDelegate. Він використовує ці методи для відслідковування кількості ходів у грі. Він скидає значення властивості numberOfTurns до нуля на початку нової гри, збільшує його на одиницю при кожному ході, та друкує загальну кількість ходів у грі, як тільки завершиться гра.

Показана вище реалізація методу gameDidStart(_:) використовує параметр game для друку деякої вступної інформації про те, що гра починається. Параметр game має тип DiceGame, а не SnakesAndLadders, тому метод gameDidStart(_:) має доступ лише до методів та властивостей, що були оголошені як частина протоколу DiceGame. Однак, все ще можна використати приведення типів всередині методу, щоб отримати тип фактично переданого екземпляру. У даному прикладі, перевіряється, чи є параметр game фактично екземпляром класу SnakesAndLadders за лаштунками, і якщо так – друкує відповідне повідомлення.

Метод gameDidStart(_:) також звертається до властивості dice переданого параметру game. Оскільки параметр game має будь-який тип, підпорядкований протоколу DiceGame, він гарантовано має властивість dice, і тому метод gameDidStart(_:) може звертатись до неї та друкувати її значення, незалежно від того, яка саме гра зараз відбувається.

Ось як виглядає DiceGameTracker в дії:

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Розпочалась нова гра у Змії та Сходи
// У грі використовуються 6-сторонні гральні кості
// На костях випало  3
// На костях випало  5
// На костях випало  4
// На костях випало  5
// Гра тривала 4 ходи(ів)

Підпорядкування протоколу за допомогою розширення

Можна розширити існуючий тип для підпорядкування його новому протоколу, навіть якщо у вас доступу до вихідного коду існуючого типу. Розширення можуть додавати нові властивості, методи та індекси до існуючих типів, і тому їм до снаги реалізувати будь-які вимоги, що можуть бути проголошені протоколом. Більше про розширення можна прочитати у розділі Розширення.

Примітка

Коли тип підпорядковується протоколу за допомогою розширення, існуючі екземпляри цього типу підпорядковуються протоколу та реалізовують його автоматично.

Наприклад, цей протокол, що називається TextRepresentable, може бути реалізованим будь-яким типом, що може бути представленим у вигляді тексту. Це може бути опис себе, або текстова версія поточного стану:

protocol TextRepresentable {
    var textualDescription: String { get }
}

Клас Dice з попередніх прикладів може бути розширеним для підпорядкування до протоколу TextRepresentable:

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "\(sides)-стороння гральна кість"
    }
}

Це розширення підпорядковує клас Dice новому протоколу так, ніби цей клас підпорядковувався би до нього у своїй оригінальній реалізації. Назва протоколу йде після імені типу, відділяючись комою, а реалізація всіх вимог протоколу йде далі у фігурних дужках.

До будь-якого екземпляру класу Dice тепер можна звертатись як до TextRepresentable:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Надрукує "12-стороння гральна кість"

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

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "Гра \"Ліла\s" на \(finalSquare) клітинок"
    }
}
print(game.textualDescription)
// Надрукує "Гра "Ліла" на 25 клітинок"

Оголошення підпорядкування протоколу за допомогою розширення

Якщо тип вже реалізовує всі вимоги протоколу, але явно не вказує, що підпорядковується до цього протоколу, можна підпорядкувати його до протоколу за допомогою порожнього розширення:

struct Hamster {
    var name: String
    var textualDescription: String {
        return "Хом'як на ім'я \(name)"
    }
}
extension Hamster: TextRepresentable {}

Екземпляри класу Hamster тепер можна використовувати будь-де, де вимагається тип TextRepresentable:

let simonTheHamster = Hamster(name: "Сомко")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Надрукує "Хом'як на ім'я Сомко"

Примітка

Типи не підпорядковуються до протоколів автоматично, лише задовольняючи його вимоги. Вони мають явно оголошувати їх підпорядкування до протоколу.

Колекції протоколів

Протокол можу використовуватись як тип, і в тому числі зберігатись у колекції на кшталт масиву чи словнику, як зазначено в Протоколи як типи. У наступному прикладі створюється масив з трьох речей, підпорядкованих протоколу TextRepresentable:

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

Тепер можливо проітерувати елементи цього масиву, і надрукувати текстовий опис кожного елементу:

for thing in things {
    print(thing.textualDescription)
}
// Гра "Ліла" на 25 клітинок
// 12-стороння гральна кість
// Хом'як на ім'я Сомко

Слід зазначити, що константа thing має тип TextRepresentable. Її тип не Dice, не DiceGame, і не Hamster, навіть якщо фактичний екземпляр за лаштунками має один з цих типів. З усім тим, оскільки вона має тип TextRepresentable, а всі підпорядковані цьому протоколу типи мають властивість textualDescription, можна безпечно звертатись до thing.textualDescription в кожній ітерації циклу.

Наслідування протоколів

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

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // тут йде визначення протоколу
}

Ось приклад протоколу, що наслідує вищезазначений протокол 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
    }
}

Це розширення підпорядковує клас SnakesAndLadders до протоколу PrettyTextRepresentable, і надає реалізацію властивості prettyTextualDescription. Будь-що, підпорядковане протоколу PrettyTextRepresentable, має бути також підпорядкованим протоколу TextRepresentable, і тому реалізація властивості prettyTextualDescription починається зі звернення до властивості textualDescription з протоколу TextRepresentable, щоб створити початковий рядок output. До цього рядка додається двокрапка та перехід на новий рядок (":\n"), і цей рядок є початковим у формуванні гарного текстового представлення. Після цього йде ітерування масиву клітинок на дошці, і додаються символи, що представляють вміст кожної клітинки:

Якщо значення клітинки більше нуля, ця клітинка представляє основу сходів, і представляється символом ▲.
Якщо значення клітинки менше нуля, ця клітинка представляє голову змії, і представляється символом ▼.
В інших випадках, значення клітинки дорівнює нулю, ця клітинка є вільною і представляється символом ○.

Властивість prettyTextualDescription тепер можна використовувати для друку гарного опису будь-якого екземпляру класу SnakesAndLadders:

print(game.prettyTextualDescription)
// Гра "Ліла" на 25 клітинок:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

Протоколи лише для класів

Можна зробити протокол доступним для підпорядкування лише класів (а не структур чи перечислень), додавши протокол AnyObject до списку наслідування протоколу.

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // тут йде визначення протоколи лише для класів
}

У прикладі вище, протоколу 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("З Днем Народження, \(celebrator.name), вам \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Максим", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Надрукує "З Днем Народження, Максим, вам 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("Привіт, \(location.name)!")
}

let seattle = City(name: "Славутич", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Надрукує "Привіт, Славутич!"

Функція beginConcert(in:) приймає параметр типу Location & Named, що означає “будь-який клас-нащадок класу Location, що підпорядковується до протоколу Named”. В даному випадку, клас City задовольняє обом цим вимогам.

Передача екземпляру birthdayPerson до функції beginConcert(in:) є некоректним, оскільки Person не є класом-нащадком класу Location. Так само, якщо створити клас-нащадок класу Location, що не підпорядковується протоколу Named, виклик функції beginConcert(in:) з екземпляром цього класу буде також некоректним.

Перевірка на підпорядкованість протоколу

Щоб перевірити тип на підпорядкованість протоколу, або привести тип до певного протоколу, можна використовувати описані в розділі Приведення типів оператори is та as. Перевірка на підпорядкованість та приведення до протоколу має точно такий же синтаксис, що й перевірка на тип чи приведення до типу:

  • Оператор 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: 603_628),
    Animal(legs: 4)
]

Цей масив objects було ініціалізовано за допомогою літералу масиву, що містить екземпляр Circle з радіусом у 2 одиниці, екземпляр Country, ініціалізований площею України у квадратних кілометрах, та екземпляр Animal, що представляє тварину з чотирма лапами.

Тепер масив objects можна проітерувати, і кожен об’єкт у масиві можна перевірити на підпорядкованість протоколу HasArea:

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Площа дорівнює \(objectWithArea.area)")
    } else {
        print("Щось, що не має площі")
    }
}
// Площа дорівнює 12.5663708
// Площа дорівнює 603628.0
// Щось, що не має площі

Для кожного об’єкту в масиві, що підпорядкований протоколу HasArea, оператор as? поверне опціональне значення, що буде розгорнуте за допомогою прив’язування опціоналу у константу на ім’я objectWithArea. Константа objectWithArea має тип HasArea, тому можна у типобезпечний спосіб звертатись до її властивості area та друкувати її значення.

Слід помітити, що об’єкти за лаштунками не змінюються внаслідок процесу приведення типів. Вони продовжують бути екземплярами Circle, Country та Animal. Однак, про посилання на один з них у константі objectWithArea відомо лише те, що воно має тип HasArea, і тому можна звертатись лише до властивостей цього типу.

Опціональні вимоги протоколів

Протокол може визначати опціональні вимоги. Ці вимоги не обов’язково повинні бути реалізованими підпорядкованими до протоколу типами. Опціональні вимоги позначаються модифікатором optional в оголошенні протоколу. Опціональні вимоги у Swift присутні головним чином через необхідність взаємодії з кодом на 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 не nil, все ще нема гарантії, що воно реалізовує метод increment(forCount:), оскільки це опціональна вимога. Тут можливість того, що метод increment(forCount:) не реалізований, опрацьовується за допомогою ланцюжка опціоналів. Виклик increment(forCount:) відбувається тільки тоді, коли він існує – тобто, коли він не nil. Тому виклик increment(forCount:) записується зі знаком питання після назви методу.

Оскільки виклик increment(forCount:) може впасти через одну з двох причин, виклик повертає опціональний Int. Це так, незважаючи на те, що у визначенні методу increment(forCount:) в протоколі CounterDataSource вказано не опціональний Int. І хоч в даному випадку ланцюжок опціоналів подвійний, результат є загорнутим лише в один опціонал. Детальніше з багаторівневими ланцюжками опціоналів можна ознайомитись у підрозділі Зв’язування кількох рівнів ланцюжків опціоналів.

Після виклику increment(forCount:), опціональне значення Int повертається і розгортається у константу amount, за допомогою прив’язування опціоналу. Якщо опціональний Int має значення – тобто, dataSource та метод існують одночасно – розгорнуте значення amount додається до властивості count, що зберігається, і виклик increment() завершується.

Якщо витягнути значення з методу increment(forCount:) неможливо – або через те, що dataSource дорівнює nil, або через те, що dataSource не реалізовує метод increment(forCount:) – тоді метод increment() пробує витягнути значення із властивості fixedIncrement. Властивість fixedIncrement також є опціональною вимогою протоколу, тому її значення – опціональний Int, хоч в оголошенні протоколу CounterDataSource вона має не опціональний тип Int.

Ось приклад реалізації протоколу 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, після чого його властивості dataSource присвоюється новий екземпляр ThreeSource, і чотири рази викликається метод лічильника increment(). Як і очікується, властивість лічильника count збільшується на три при кожному виклику методу increment().

Ось приклад складнішого джерела даних у вигляді класу TowardsZeroSource, що змушує екземпляр Counter змінювати значення count в сторону нуля:

@objc 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() для повернення випадкового булевого значення:

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

Створення розширення протоколу автоматично наділяє всі підпорядковані типи реалізаціями цього методу без їх додаткової модифікації.

let generator = LinearCongruentialGenerator()
print("Це випадкове число: \(generator.random())")
// Надрукує "Це випадкове число: 0.3746499199817101"
print("А це – випадкове булеве значення: \(generator.randomBool())")
// Надрукує "А це – випадкове булеве значення: true"

Розширення протоколів можуть додавати реалізації до підпорядкованих типів, за допомогою них не можна розширити протокол новими вимогами, чи успадкувати інший протокол. Наслідування завжди повинно бути зазначеним в оголошенні самого протоколу.

Реалізації вимог за замовчанням

За допомогою розширення протоколу можна надавати реалізації за замовчанням для будь-яких вимог методів чи властивостей, що обчислюються цього протоколу. Якщо підпорядкований тип має власну реалізацію вимоги методу чи властивості, ця реалізація буде використовуватись замість реалізації з розширення протоколу.

Примітка

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

Наприклад, протокол PrettyTextRepresentable, що наслідує протокол TextRepresentable, може мати реалізацію вимоги властивості prettyTextualDescription, котра просто повертатиме результат доступу до властивості textualDescription:

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

Додавання обмежень до розширень протоколів

При оголошенні розширення протоколу, можна вказати обмеження, які накладаються на підпорядковані типи, котрим будуть доступні методи та властивості з розширення протоколу. Ці обмеження записуються після назви протоколу, що розширюється, за допомогою інструкції узагальнення where. Детальніше з нею можна ознайомитись у підрозділі Інструкція узагальнення 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())
// Надрукує "true"
print(differentNumbers.allEqual())
// Надрукує "false"

Примітка

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