Перейти к содержимому

Лекция 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 = true
print(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) // 0

7.3. Правила делегирования

  1. Designated вызывает designated родителя (super.init).
  2. Convenience вызывает другой инициализатор своего класса (self.init).
  3. Цепочка 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.0

11. Сравнение с Python

АспектSwiftPython
Множественное наследованиеНет (протоколы)Да
Обязательный суперклассНет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 — определяется MRO
print(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) } // IT

14. Упражнения

Упражнение 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. Вопросы для самопроверки

  1. Почему структуры в Swift не поддерживают наследование?
  2. Что произойдёт, если убрать override при переопределении метода?
  3. Чем as? отличается от as!? Когда использовать каждый?
  4. Объясните разницу между designated и convenience инициализаторами.
  5. Что такое двухфазная инициализация? Почему нельзя обращаться к self до super.init()?
  6. Для чего используется final? Какие преимущества даёт компилятору?
  7. Назовите три отличия модели наследования Swift от Python.
  8. Что такое MRO и почему в Swift он не нужен?
  9. Когда вызывается deinit? Чем он отличается от __del__ в Python?
  10. Приведите пример полиморфизма с переменной базового типа.

16. Итоги

В этой лекции мы изучили:

  • Наследование — механизм только для классов: class Child: Parent.
  • Переопределение методов и свойств через явное override.
  • final — запрет переопределения и наследования.
  • Инициализацию — designated, convenience, required init, двухфазная модель.
  • Приведение типовis, as, as?, as!.
  • Полиморфизм — динамическую диспетчеризацию через переменные базового типа.
  • Деинициализаторы deinit — очистку ресурсов при освобождении объекта.

В следующей лекции мы рассмотрим перечисления (Enumerations) и Pattern Matching — мощную систему типов Swift, значительно превосходящую Enum в Python.