Лекция 8. Наследование и полиморфизм
1. Введение
Наследование — механизм ООП, позволяющий создавать новые типы на основе существующих. В Swift наследование работает только для классов — структуры и перечисления его не поддерживают. В этой лекции мы рассмотрим наследование, переопределение, инициализацию в иерархиях классов, приведение типов, полиморфизм и деинициализаторы, сравнивая подходы Swift и Python.
2. Базовый класс
В отличие от Python, где все классы неявно наследуют от object, в Swift нет обязательного суперкласса:
class Animal { var name: String var sound: String
init(name: String, sound: String) { self.name = name self.sound = sound }
func describe() -> String { return "\(name) издаёт звук: \(sound)" }}Сравнение с Python:
class Animal: # неявно наследует от object def __init__(self, name: str, sound: str): self.name = name self.sound = sound
def describe(self) -> str: return f"{self.name} издаёт звук: {self.sound}"3. Наследование: синтаксис
Дочерний класс объявляется через двоеточие — class Child: Parent:
class Dog: Animal { var breed: String
init(name: String, breed: String) { self.breed = breed super.init(name: name, sound: "Гав!") }
func fetch() -> String { return "\(name) приносит мяч!" }}
let dog = Dog(name: "Рекс", breed: "Овчарка")print(dog.describe()) // Рекс издаёт звук: Гав!print(dog.fetch()) // Рекс приносит мяч!Важно: Swift поддерживает только одиночное наследование. Для множественного поведения — протоколы (лекция 10). Структуры и перечисления наследовать не могут — это value types.
4. Переопределение методов: override
Swift требует явного override — защита от случайного переопределения:
class Cat: Animal { init(name: String) { super.init(name: name, sound: "Мяу!") }
override func describe() -> String { return "Кот \(name) говорит: \(sound)" }}print(Cat(name: "Мурзик").describe()) // Кот Мурзик говорит: Мяу!Забыли override — ошибка компиляции. Написали override для несуществующего метода — тоже ошибка. В Python переопределение неявное.
4.1. Вызов родительской реализации: super
class ServiceDog: Dog { var task: String
init(name: String, breed: String, task: String) { self.task = task super.init(name: name, breed: breed) }
override func describe() -> String { return super.describe() + " Задача: \(task)." }}5. Переопределение свойств: override var
class Vehicle { var speed: Double = 0.0 var description: String { return "Скорость: \(speed) км/ч" }}
class Bicycle: Vehicle { var hasBasket = false override var description: String { return super.description + (hasBasket ? ", с корзиной" : "") }}
class SpeedMonitor: Vehicle { override var speed: Double { didSet { if speed > 100 { print("Превышение!") } } }}
let bike = Bicycle()bike.speed = 15.0; bike.hasBasket = trueprint(bike.description) // Скорость: 15.0 км/ч, с корзиной6. Предотвращение переопределения: final
final запрещает переопределение метода, свойства или наследование от класса целиком:
class Base { final func criticalMethod() { print("Нельзя переопределить") }}
final class Singleton { static let shared = Singleton() private init() {}}// class Child: Singleton {} // Ошибка компиляции!final повышает безопасность и позволяет компилятору оптимизировать вызовы. В Python @final (с 3.8) носит лишь рекомендательный характер.
7. Инициализация при наследовании
7.1. Designated initializers (назначенные)
Основной инициализатор. Инициализирует все свои свойства и вызывает super.init(...).
7.2. Convenience initializers (удобные)
Помечаются convenience, обязаны вызвать другой инициализатор того же класса:
class Person { var name: String var age: Int init(name: String, age: Int) { // designated self.name = name; self.age = age } convenience init(name: String) { // convenience self.init(name: name, age: 0) }}let baby = Person(name: "Малыш")print(baby.age) // 07.3. Правила делегирования
- Designated вызывает designated родителя (
super.init). - Convenience вызывает другой инициализатор своего класса (
self.init). - Цепочка convenience → … → designated → super.init.
7.4. required init
Обязывает каждый дочерний класс реализовать данный инициализатор:
class View { required init(frame: String) { }}class Button: View { required init(frame: String) { super.init(frame: frame) }}8. Двухфазная инициализация
Фаза 1 (снизу вверх): дочерний класс инициализирует свои свойства → вызывает super.init → родитель инициализирует свои и так далее до базового.
Фаза 2 (сверху вниз): каждый класс может дополнительно настроить свойства и обратиться к self.
class Transport { var type: String init(type: String) { self.type = type }}
class Car: Transport { var brand: String init(brand: String) { self.brand = brand // Фаза 1: своё свойство super.init(type: "Автомобиль") // Фаза 1: родитель print("Марка: \(self.brand)") // Фаза 2: self доступен }}Важно: до super.init(...) нельзя обращаться к self и унаследованным свойствам — компилятор проверяет. В Python таких ограничений нет.
9. Приведение типов
class MediaItem { var name: String init(name: String) { self.name = name }}class Movie: MediaItem { var director: String init(name: String, director: String) { self.director = director; super.init(name: name) }}class Song: MediaItem { var artist: String init(name: String, artist: String) { self.artist = artist; super.init(name: name) }}
let library: [MediaItem] = [ Movie(name: "Сталкер", director: "Тарковский"), Song(name: "Катюша", artist: "Лидия Русланова"),]9.1. is — проверка, as? — опциональное приведение, as! — принудительное
for item in library { if item is Movie { print("\(item.name) — фильм") } // is if let song = item as? Song { print(song.artist) } // as?}let first = library[0] as! Movie // as! — уверены в типеprint(first.director) // Тарковский// let wrong = library[1] as! Movie // Аварийное завершение!В Python: isinstance() для проверки типа. Duck typing позволяет обращаться к атрибутам без приведения (с риском AttributeError).
10. Полиморфизм
Переменная базового типа, вызов метода дочернего — динамическая диспетчеризация:
class Shape { func area() -> Double { return 0.0 } func describe() -> String { return "Площадь: \(area())" }}
class Circle: Shape { var radius: Double init(radius: Double) { self.radius = radius } override func area() -> Double { return Double.pi * radius * radius }}
class Rectangle: Shape { var width, height: Double init(width: Double, height: Double) { self.width = width; self.height = height } override func area() -> Double { return width * height }}
let shapes: [Shape] = [Circle(radius: 5.0), Rectangle(width: 4.0, height: 6.0)]for shape in shapes { print(shape.describe()) }// Площадь: 78.53981633974483// Площадь: 24.011. Сравнение с Python
| Аспект | Swift | Python |
|---|---|---|
| Множественное наследование | Нет (протоколы) | Да |
| Обязательный суперкласс | Нет | object |
| Переопределение | Явное: override | Неявное |
| Запрет переопределения | final | @final (подсказка) |
| Приведение типов | is, as?, as! | isinstance() |
| MRO | Не нужен | C3-линеаризация |
| Типизация | Статическая | Duck typing |
11.1. Diamond problem и MRO в Python
class A: def greet(self): print("A")class B(A): def greet(self): print("B")class C(A): def greet(self): print("C")class D(B, C): pass
D().greet() # B — определяется MROprint(D.mro()) # [D, B, C, A, object]В Swift проблема исключена — множественное наследование классов запрещено.
12. Деинициализаторы: deinit
Вызывается автоматически при освобождении объекта (ARC). Только для классов, без параметров:
class FileHandler { var filename: String init(filename: String) { self.filename = filename print("Файл \(filename) открыт") } deinit { print("Файл \(filename) закрыт") }}
do { let handler = FileHandler(filename: "data.txt") print("Работа с файлом...")}// Файл data.txt открыт// Работа с файлом...// Файл data.txt закрытВ Python __del__ вызывается сборщиком мусора, момент не гарантирован. Предпочтительнее with-менеджеры.
13. Комплексный пример
Объединим наследование, override, convenience init, final, deinit, полиморфизм и приведение типов:
class Employee { var name: String var salary: Double init(name: String, salary: Double) { self.name = name; self.salary = salary } convenience init(name: String) { self.init(name: name, salary: 30000.0) } func bonus() -> Double { return salary * 0.1 } func info() -> String { return "\(name), бонус: \(bonus())" } deinit { print("\(name) удалён") }}
class Manager: Employee { var dept: String init(name: String, salary: Double, dept: String) { self.dept = dept; super.init(name: name, salary: salary) } override func bonus() -> Double { return salary * 0.2 } override func info() -> String { return super.info() + ", отдел: \(dept)" }}
final class Intern: Employee { override func bonus() -> Double { return 0 }}
let team: [Employee] = [ Manager(name: "Ольга", salary: 80000, dept: "IT"), Intern(name: "Алексей"),]for m in team { print(m.info()) }// Ольга, бонус: 16000.0, отдел: IT// Алексей, бонус: 0.0
if let mgr = team[0] as? Manager { print(mgr.dept) } // IT14. Упражнения
Упражнение 1. Создайте иерархию Transport -> Car -> ElectricCar. Каждый класс переопределяет describe(). ElectricCar добавляет batteryLevel: Int.
Упражнение 2. Создайте BankAccount с designated и convenience инициализаторами. Дочерний SavingsAccount добавляет interestRate и метод addInterest().
Упражнение 3. Создайте массив фигур (Circle, Rectangle, Triangle). Используя is и as?, посчитайте количество каждого типа и выведите площади.
Упражнение 4. Создайте Logger с deinit. Продемонстрируйте вызов деинициализатора через блок do { }.
Упражнение 5. Перепишите на Swift с override и полиморфизмом:
class Notification: def __init__(self, msg): self.msg = msg def send(self): print(f"Уведомление: {self.msg}")
class EmailNotification(Notification): def __init__(self, msg, email): super().__init__(msg); self.email = email def send(self): print(f"Email на {self.email}: {self.msg}")
class SMSNotification(Notification): def __init__(self, msg, phone): super().__init__(msg); self.phone = phone def send(self): print(f"SMS на {self.phone}: {self.msg}")15. Вопросы для самопроверки
- Почему структуры в Swift не поддерживают наследование?
- Что произойдёт, если убрать
overrideпри переопределении метода? - Чем
as?отличается отas!? Когда использовать каждый? - Объясните разницу между designated и convenience инициализаторами.
- Что такое двухфазная инициализация? Почему нельзя обращаться к
selfдоsuper.init()? - Для чего используется
final? Какие преимущества даёт компилятору? - Назовите три отличия модели наследования Swift от Python.
- Что такое MRO и почему в Swift он не нужен?
- Когда вызывается
deinit? Чем он отличается от__del__в Python? - Приведите пример полиморфизма с переменной базового типа.
16. Итоги
В этой лекции мы изучили:
- Наследование — механизм только для классов:
class Child: Parent. - Переопределение методов и свойств через явное
override. final— запрет переопределения и наследования.- Инициализацию — designated, convenience,
required init, двухфазная модель. - Приведение типов —
is,as,as?,as!. - Полиморфизм — динамическую диспетчеризацию через переменные базового типа.
- Деинициализаторы
deinit— очистку ресурсов при освобождении объекта.
В следующей лекции мы рассмотрим перечисления (Enumerations) и Pattern Matching — мощную систему типов Swift, значительно превосходящую Enum в Python.