Протоколы
Чему вы научитесь:
- описывать протоколом, что должен уметь тип, и объявлять, что ваши структуры ему соответствуют;
- хранить значения разных типов в одном массиве
[any Shape]и вызывать у каждого его собственный метод; - добавлять умения готовым типам и давать протоколу реализацию по умолчанию;
- принимать стандартные протоколы и писать обобщённые функции с ограничением типа.

Представьте доску объявлений, а на ней вакансию «Фигура». В описании работы одна задача: уметь посчитать свою площадь. Кто это умеет, тот получает работу и носит значок с названием вакансии.
На работу приходят круг, квадрат и треугольник. Площадь каждый считает по-своему: квадрат умножает сторону на сторону, а треугольник берёт половину произведения основания на высоту. Работодателю всё равно, как именно. Ему важно, что любой со значком ответит на вопрос «Какая у тебя площадь?».
Зачем это программе? Например, нужно сложить площади всех фигур на чертеже. Квадрат и круг — разные структуры, а массив хранит значения одного типа. Общего родителя, как Animal у собаки и кошки в массиве [Animal], у структур не бывает. Нужен общий тип «любая фигура», и в Swift его задаёт протокол.
Протокол — описание работы
Протокол перечисляет, что должен уметь тип, но не говорит, как это делать. Каждый пункт такого списка называют требованием. Опишем вакансию «Фигура» и примем на неё квадрат:
protocol Shape {
var name: String { get }
func area() -> Double
}
struct Square: Shape {
let name = "square"
let side: Double
func area() -> Double { side * side }
}
let tile = Square(side: 3)
print(tile.name, tile.area()) // square 9.0Давайте разберём программу по частям:
protocol Shapeобъявляет протокол. Его имя, как имя типа, пишут с большой буквы.var name: String { get }— требование-свойство: у фигуры должно быть свойствоname, которое можно прочитать.func area() -> Double— требование-метод. Тела у него нет: как считать площадь, решит сам тип.struct Square: Shape— значок на квадрате. Двоеточие и имя протокола после имени типа говорят, что квадрат берётся за эту работу.- Внутри
Squareесть иname, иarea(), поэтому компилятор согласен: требования выполнены.
Тип, который выполнил все требования протокола и объявил это через двоеточие, соответствует протоколу. Значок без нужных умений компилятор не пропустит.
Свойство из требования
Требование { get } говорит только одно: свойство можно прочитать. Как его хранить, тип решает сам:
- У квадрата
name— хранимая константаlet name = "square". Значение у неё уже есть и другим не станет, поэтому квадрат создают безname:Square(side: 3). - Круг может вычислять его каждый раз:
var name: String { "circle" }.
В самом протоколе свойство всегда объявляют через var, даже если его будут только читать: об этом говорит { get }. Запись let в протоколе не скомпилируется.
Протокол сам ничего не хранит. Свойство name живёт в квадрате, а протокол только требует, чтобы оно было.
Протокол как тип
Имя протокола можно писать там, где пишут тип. Запись any Shape значит «значение любого типа, который соответствует Shape». Добавим круг и положим квадрат и круг в один массив:
struct Circle: Shape {
let radius: Double
var name: String { "circle" }
func area() -> Double { 3.14 * radius * radius }
}
let shapes: [any Shape] = [tile, Circle(radius: 2)]
for shape in shapes {
print(shape.name, shape.area(), terminator: "; ") // square 9.0; circle 12.56;
}Цикл не знает, квадрат перед ним или круг. Он вызывает shape.area(), а Swift сам находит метод того типа, чьё значение лежит в shape сейчас. Без протокола такой массив не собрать: Square и Circle — разные типы, и общего типа у них нет.
Нажимайте «Следующий шаг» и следите за столбиком рамок: одна и та же строка shape.area() ведёт то в метод квадрата, то в метод треугольника.
1protocol Shape {2 func area() -> Double3}4struct Square: Shape {5 let side: Double6 func area() -> Double { side * side }7}8struct Triangle: Shape {9 let base: Double10 let height: Double11 func area() -> Double {12 base * height / 213 }14}15let shapes: [any Shape] = [16 Square(side: 3),17 Triangle(base: 4, height: 5),18]19for shape in shapes {20 print(shape.area())21}
В массиве [any Shape] лежат значения двух разных типов: квадрат и треугольник.
Слово any подчёркивает, что в массиве могут лежать значения разных типов. В старом коде встречается запись без него, [Shape], и она значит то же самое.
Здесь картинка с вакансией неточна. Человек может уметь нужное и без значка. А в Swift тип получает работу, только если сам объявил соответствие: структуру с методом area(), но без : Shape в массив [any Shape] не положить.
Протокол или базовый класс
Этот вопрос часто задают на собеседовании: чем протокол отличается от базового класса? Базовым называют родительский класс, от которого наследуют подклассы. Главных отличий три:
- от класса наследуют только классы, а протоколу могут соответствовать структуры, перечисления и классы;
- родительский класс у класса один, а протоколов тип может принять несколько, через запятую:
struct Square: Shape, Named, если есть ещё протоколNamed; - базовый класс передаёт подклассам свои хранимые свойства, а протокол ничего не хранит: свойства из требований хранит сам тип.
Частая ошибка
Значок надели, а требование выполнили не так, как написано в протоколе: площадь треугольника вернули как Int.
struct Triangle: Shape { // ✗
let name = "triangle"
let base: Double
let height: Double
func area() -> Int { Int(base * height / 2) }
}error: type 'Triangle' does not conform to protocol 'Shape'Программа не компилируется. Протокол требует area() -> Double, а у треугольника метод с другим типом результата, и требование он не выполняет. Компилятор сверяет требования до запуска. Исправление — вернуть Double: func area() -> Double { base * height / 2 }.
Главное
- Протокол перечисляет требования к свойствам и методам, а как их выполнить, решает тип.
- Тип соответствует протоколу, когда объявил это через двоеточие и выполнил все требования; компилятор проверяет это до запуска.
- В массиве
[any Shape]лежат значения разных типов, и вызовshape.area()попадает в метод того типа, чьё значение лежит вshape. - В отличие от базового класса, протокол ничего не хранит, ему соответствуют и структуры с перечислениями, а тип может принять несколько протоколов.