Реализация по умолчанию
В описание вакансии «Фигура» добавили вторую задачу: рассказать о себе одной строкой. Почти все фигуры ответят одинаково: назовут свою площадь. Писать одну и ту же строку в каждом типе скучно, а в копии недолго ошибиться.
Поэтому к описанию работы прикладывают типовую инструкцию: «Если у вас нет своего способа, делайте так». Кто знает, как лучше, работает по-своему. Остальные берут инструкцию и не пишут ничего.
В 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Давайте разберём программу по частям:
- В протоколе два требования:
area()иdescribe(). extension Shapeрасширяет сам протокол. Методdescribe()в этом расширении — реализация по умолчанию.- Внутри она вызывает
area(). Про сторону квадрата инструкция ничего не знает, но знает, что у любой фигуры естьarea(). Squareпишет толькоarea(). Методdescribe()квадрат получает из расширения, и компилятор считает оба требования выполненными.
Здесь картинка с инструкцией неточна. Живой сотрудник может прочитать инструкцию и что-то в ней поправить под себя. Реализация по умолчанию одинакова для всех и пользуется только тем, что требует протокол: про side или radius она не знает.
Свой метод вместо типового
Тип, у которого есть свой способ, пишет метод сам. Тогда типовая инструкция ему не нужна:
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() в первом уроке темы.
Частая ошибка
Метод дописали только в расширение протокола, а в список требований не добавили. Круг написал свою версию, и вы ждёте, что вызовется она:
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всегда берётся из расширения протокола.