Лекция 4. Наследование, переопределение и полиморфизм
1. Зачем нужна иерархия типов
Наследование позволяет описать отношение «является разновидностью». Подкласс получает доступное состояние и поведение базового класса, уточняет его и при необходимости заменяет отдельные операции.
class Transport { let title: String
init(title: String) { self.title = title }
func move() -> String { "Транспорт движется" }}
final class Bicycle: Transport { override func move() -> String { "Велосипед движется за счёт велосипедиста" }}Наследование уместно, если объект подкласса можно использовать везде, где ожидается базовый класс, не нарушая смысл программы.
2. Что наследует подкласс
Подкласс наследует методы, вычисляемые свойства и доступные хранимые свойства. Он не наследует инициализаторы автоматически во всех случаях: правила зависят от собственных свойств и designated initializer подкласса.
class Employee { let name: String
init(name: String) { self.name = name }
var description: String { name }}
final class Developer: Employee { let language: String
init(name: String, language: String) { self.language = language super.init(name: name) }
override var description: String { "\(name), разработчик на \(language)" }}Сначала подкласс формирует собственное состояние, затем делегирует создание базовой
части объекта через super.init.
3. Переопределение
Ключевое слово override явно показывает, что член уже существует в базовом классе.
Компилятор проверяет имя, сигнатуру и совместимость типа результата.
class Notification { func send(to recipient: String) -> String { "Сообщение для \(recipient)" }}
final class EmailNotification: Notification { override func send(to recipient: String) -> String { "Email отправлен на \(recipient)" }}Без override случайное совпадение имени могло бы скрыть ошибку проектирования.
Вызов реализации базового класса
Иногда новое поведение дополняет, а не полностью заменяет старое.
class LoggedNotification: Notification { override func send(to recipient: String) -> String { let result = super.send(to: recipient) return "[log] \(result)" }}Вызов super должен иметь содержательный смысл. Если почти каждый метод подкласса
обходит или отменяет поведение базы, иерархия выбрана неудачно.
4. Полиморфизм
Переменная базового типа может хранить экземпляр любого допустимого подкласса. Конкретная реализация переопределённого метода выбирается во время выполнения.
class Shape { func area() -> Double { 0 }}
final class Circle: Shape { let radius: Double
init(radius: Double) { self.radius = radius }
override func area() -> Double { Double.pi * radius * radius }}
final class Rectangle: Shape { let width: Double let height: Double
init(width: Double, height: Double) { self.width = width self.height = height }
override func area() -> Double { width * height }}
let shapes: [Shape] = [Circle(radius: 2), Rectangle(width: 3, height: 4)]let total = shapes.reduce(0) { $0 + $1.area() }Клиент работает с общей абстракцией и не содержит switch по конкретным типам.
5. Статический и динамический тип
let shape: Shape = Circle(radius: 3)Статический тип переменной — Shape: он определяет, какие члены разрешено вызывать
компилятору. Динамический тип объекта — Circle: он определяет, какая
переопределённая реализация выполнится.
Метод, существующий только у Circle, нельзя вызвать через переменную Shape без
проверки и приведения типа.
6. Проверка и приведение типов
Оператор is проверяет тип, as? выполняет безопасное приведение, as! требует
успеха и аварийно завершает программу при ошибке.
for shape in shapes { if let circle = shape as? Circle { print("Радиус: \(circle.radius)") }}Частая потребность в as? означает, что общей абстракции, вероятно, не хватает
нужной операции. Сначала стоит попробовать выразить поведение полиморфным методом.
7. final и управление расширяемостью
final class запрещает создание подклассов. final у метода запрещает только его
переопределение.
class Account { final func validateIdentifier(_ value: String) -> Bool { !value.isEmpty }}По умолчанию класс полезно делать final, если расширение через наследование не
является частью замысла. Это уменьшает число возможных вариантов поведения.
8. Абстрактные классы в Swift
В Swift нет ключевого слова abstract. Общий класс можно выразить методом с
заглушкой, но такая конструкция допускает создание неполезного экземпляра.
class Exporter { func export(_ values: [String]) -> String { fatalError("Подкласс должен переопределить export") }}Чаще обязательный контракт лучше моделировать протоколом. Наследование следует оставлять для действительно общего состояния и готовой реализации.
9. Подстановка и корректная иерархия
Подкласс не должен усиливать предусловия, ослаблять обещанный результат или ломать инварианты базового класса.
Если метод базового класса обещает сохранить документ любого допустимого размера, подкласс не должен неожиданно принимать только маленькие документы. Иначе клиент, работающий с базовым типом, получит отказ.
Хорошая проверка иерархии: можно ли заменить базовый объект подклассом без новых проверок в клиентском коде?
10. Наследование или композиция
Наследование фиксирует сильную связь между типами. Композиция собирает объект из независимых ролей.
final class Engine { func start() -> String { "Двигатель запущен" }}
final class Car { private let engine: Engine
init(engine: Engine) { self.engine = engine }
func start() -> String { engine.start() }}Выбирайте наследование для устойчивого отношения «является», а композицию — для отношения «имеет» или сменяемой стратегии поведения.
Типичные ошибки
- наследование используется только ради повторного использования кода;
- базовый класс знает о конкретных подклассах;
- подкласс отменяет большую часть поведения родителя;
- клиент постоянно выполняет приведения типов;
- открытая иерархия создаётся без необходимости;
as!применяется к данным, которые не контролируются программой.
Вопросы для самопроверки
- Какое отношение предметной области выражает наследование?
- Зачем Swift требует писать
override? - Чем статический тип отличается от динамического?
- Как выбирается реализация переопределённого метода?
- Когда нужен вызов
super? - Чем
as?отличается отas!? - Что запрещает
final? - Почему частые приведения типов являются сигналом проблемы?
- Как проверить корректность подстановки подкласса?
- Когда композиция предпочтительнее наследования?
Итоги
- наследование описывает специализацию базового класса;
- переопределение меняет реализацию сохранённого контракта;
- полиморфизм позволяет клиенту работать с общей абстракцией;
- безопасное приведение выполняется через
as?; finalявно ограничивает точки расширения;- корректный подкласс не ломает ожидания клиентов базового типа;
- композиция часто даёт более гибкую и слабосвязанную модель.