Перейти к содержимому
Теория · Протоколы и обобщённый код · урок 1 из 9

Протоколы

Чему вы научитесь

Чему вы научитесь:

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

Представьте доску объявлений, а на ней вакансию «Фигура». В описании работы одна задача: уметь посчитать свою площадь. Кто это умеет, тот получает работу и носит значок с названием вакансии.

На работу приходят круг, квадрат и треугольник. Площадь каждый считает по-своему: квадрат умножает сторону на сторону, а треугольник берёт половину произведения основания на высоту. Работодателю всё равно, как именно. Ему важно, что любой со значком ответит на вопрос «Какая у тебя площадь?».

Зачем это программе? Например, нужно сложить площади всех фигур на чертеже. Квадрат и круг — разные структуры, а массив хранит значения одного типа. Общего родителя, как Animal у собаки и кошки в массиве [Animal], у структур не бывает. Нужен общий тип «любая фигура», и в Swift его задаёт протокол.

Протокол — описание работы

Протокол перечисляет, что должен уметь тип, но не говорит, как это делать. Каждый пункт такого списка называют требованием. Опишем вакансию «Фигура» и примем на неё квадрат:

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

Давайте разберём программу по частям:

  1. protocol Shape объявляет протокол. Его имя, как имя типа, пишут с большой буквы.
  2. var name: String { get } — требование-свойство: у фигуры должно быть свойство name, которое можно прочитать.
  3. func area() -> Double — требование-метод. Тела у него нет: как считать площадь, решит сам тип.
  4. struct Square: Shape — значок на квадрате. Двоеточие и имя протокола после имени типа говорят, что квадрат берётся за эту работу.
  5. Внутри Square есть и name, и area(), поэтому компилятор согласен: требования выполнены.

Тип, который выполнил все требования протокола и объявил это через двоеточие, соответствует протоколу. Значок без нужных умений компилятор не пропустит.

Свойство из требования

Требование { get } говорит только одно: свойство можно прочитать. Как его хранить, тип решает сам:

  • У квадрата name — хранимая константа let name = "square". Значение у неё уже есть и другим не станет, поэтому квадрат создают без name: Square(side: 3).
  • Круг может вычислять его каждый раз: var name: String { "circle" }.

В самом протоколе свойство всегда объявляют через var, даже если его будут только читать: об этом говорит { get }. Запись let в протоколе не скомпилируется.

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

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

Имя протокола можно писать там, где пишут тип. Запись any Shape значит «значение любого типа, который соответствует Shape». Добавим круг и положим квадрат и круг в один массив:

SWIFT
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() ведёт то в метод квадрата, то в метод треугольника.

ВживуюКаждая фигура считает площадь самашаг 1 из 8 · рамок: 1

В массиве [any Shape] лежат значения двух разных типов: квадрат и треугольник.

Смотрите на верхнюю рамку: её имя меняется, хотя вызов один и тот же

Слово any подчёркивает, что в массиве могут лежать значения разных типов. В старом коде встречается запись без него, [Shape], и она значит то же самое.

Здесь картинка с вакансией неточна. Человек может уметь нужное и без значка. А в Swift тип получает работу, только если сам объявил соответствие: структуру с методом area(), но без : Shape в массив [any Shape] не положить.

Протокол или базовый класс

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

  • от класса наследуют только классы, а протоколу могут соответствовать структуры, перечисления и классы;
  • родительский класс у класса один, а протоколов тип может принять несколько, через запятую: struct Square: Shape, Named, если есть ещё протокол Named;
  • базовый класс передаёт подклассам свои хранимые свойства, а протокол ничего не хранит: свойства из требований хранит сам тип.

Частая ошибка

Значок надели, а требование выполнили не так, как написано в протоколе: площадь треугольника вернули как Int.

SWIFT
struct Triangle: Shape { // ✗
    let name = "triangle"
    let base: Double
    let height: Double
    func area() -> Int { Int(base * height / 2) }
}
TEXT
error: type 'Triangle' does not conform to protocol 'Shape'

Программа не компилируется. Протокол требует area() -> Double, а у треугольника метод с другим типом результата, и требование он не выполняет. Компилятор сверяет требования до запуска. Исправление — вернуть Double: func area() -> Double { base * height / 2 }.

Главное

  • Протокол перечисляет требования к свойствам и методам, а как их выполнить, решает тип.
  • Тип соответствует протоколу, когда объявил это через двоеточие и выполнил все требования; компилятор проверяет это до запуска.
  • В массиве [any Shape] лежат значения разных типов, и вызов shape.area() попадает в метод того типа, чьё значение лежит в shape.
  • В отличие от базового класса, протокол ничего не хранит, ему соответствуют и структуры с перечислениями, а тип может принять несколько протоколов.