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

Реализация по умолчанию

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

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

В Swift такая инструкция — метод в расширении протокола. Его называют реализацией по умолчанию: тип, который не написал свой метод, получает этот.

Метод из расширения протокола

Расширять можно не только типы, но и сами протоколы. Опишем вторую задачу и дадим ей типовую инструкцию:

SWIFT
protocol Shape {
    func area() -> Double
    func describe() -> String
}

extension Shape {
    func describe() -> String { "area \(area())" }
}

struct Square: Shape {
    let side: Double
    func area() -> Double { side * side }
}

print(Square(side: 2).describe()) // area 4.0

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

  1. В протоколе два требования: area() и describe().
  2. extension Shape расширяет сам протокол. Метод describe() в этом расширении — реализация по умолчанию.
  3. Внутри она вызывает area(). Про сторону квадрата инструкция ничего не знает, но знает, что у любой фигуры есть area().
  4. Square пишет только area(). Метод describe() квадрат получает из расширения, и компилятор считает оба требования выполненными.

Здесь картинка с инструкцией неточна. Живой сотрудник может прочитать инструкцию и что-то в ней поправить под себя. Реализация по умолчанию одинакова для всех и пользуется только тем, что требует протокол: про side или radius она не знает.

Свой метод вместо типового

Тип, у которого есть свой способ, пишет метод сам. Тогда типовая инструкция ему не нужна:

SWIFT
struct Circle: Shape {
    let radius: Double
    func area() -> Double { 3.14 * radius * radius }
    func describe() -> String { "circle, radius \(radius)" }
}

let shapes: [any Shape] = [Square(side: 2), Circle(radius: 1)]
for shape in shapes {
    print(shape.describe(), terminator: "; ") // area 4.0; circle, radius 1.0;
}

Квадрат рассказал о себе по инструкции, а круг — своими словами. Метод, который тип написал сам, заменяет реализацию по умолчанию. Через any Shape это тоже работает. describe() есть в списке требований, поэтому Swift находит метод того типа, чьё значение лежит в shape, как с area() в первом уроке темы.

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

Метод дописали только в расширение протокола, а в список требований не добавили. Круг написал свою версию, и вы ждёте, что вызовется она:

SWIFT
extension Shape {
    func summary() -> String { "a shape" }
}

extension Circle {
    func summary() -> String { "a circle" }
}

print(Circle(radius: 1).summary()) // a circle
for shape in shapes {
    print(shape.summary(), terminator: "; ") // ✗
}

Программа компилируется, но цикл печатает a shape; a shape; — и для квадрата, и для круга. summary() нет среди требований протокола. Про значение типа any Shape Swift знает только то, что у него есть требования протокола и методы из расширения протокола. Поэтому вызывается версия из расширения, даже если в shape лежит круг. Прямой вызов Circle(radius: 1).summary() при этом даёт a circle.

Этот случай любят на собеседованиях: какой метод вызовется через any Shape, если в протоколе его нет? Ответ — метод из расширения протокола. Исправление — добавить func summary() -> String в протокол рядом с describe(). Тогда через any Shape будет вызываться метод круга.

Главное

  • Метод в расширении протокола — реализация по умолчанию: её получает каждый тип, который не написал свою.
  • Метод, который тип написал сам, заменяет реализацию по умолчанию, и через any Shape вызывается он, если метод есть среди требований.
  • Реализация по умолчанию пользуется только тем, что требует протокол.
  • Метод, которого нет в требованиях, через any Shape всегда берётся из расширения протокола.