Как реализовать множественное наследование через несколько протоколов в Swift
Что проверяют
Понимаете ли вы, что протоколы с расширениями заменяют множественное наследование, и знаете ли их ограничения.
Как отвечать
Класс наследует один класс, но может принимать сколько угодно протоколов. Реализация по умолчанию задаётся в расширении протокола, и это даёт наследование поведения из нескольких источников.
protocol Identifiable { var id: String { get } }
protocol Loggable {}
extension Loggable {
func log(_ message: String) { print(message) }
}
struct Job: Identifiable, Loggable {
let id: String
}Тип получил и требование, и готовую реализацию, не наследуя ни одного класса. Отсюда и подход, который называют протокол-ориентированным: поведение собирается из независимых кусочков, а не тянется по цепочке наследования.
Ограничения, которые надо назвать:
- у протоколов нет хранимых свойств. В расширении можно объявить только вычисляемое. Состояние обязан хранить сам тип, поэтому «примесь с полем» не сделать напрямую;
- конфликт реализаций не разрешается автоматически. Если два протокола дают в расширениях метод с одинаковой сигнатурой, компилятор потребует явную реализацию в самом типе. Это лучше, чем молчаливый выбор, но требует внимания;
- диспетчеризация зависит от объявления, и это самая коварная ловушка. Метод, объявленный в самом протоколе и переопределённый в типе, вызывается динамически. Метод, существующий только в расширении, диспетчеризуется статически: при вызове через переменную типа протокола выполнится реализация из расширения, даже если тип определил свою.
Полезные дополнения: композиция протоколов записывается через амперсанд, а ограничение расширения через where позволяет давать реализацию только тем типам, которые удовлетворяют условию.
Где спотыкаются
Пытаются объявить хранимое свойство в расширении протокола.
Не знают про статическую диспетчеризацию методов из расширения.
Строят глубокие иерархии протоколов вместо плоского набора.
Считают, что конфликт одинаковых методов разрешится сам.