Перейти к содержимому
Теория · Классы · урок 5 из 10

Наследование

Собака — это животное. Всё, что есть у любого животного, есть и у собаки: например, кличка и голос. Но голос у собаки особый — «гав».

Теперь представьте программу с классами Dog, Cat и Parrot, и у каждого — кличка, init и метод speak(). Писать одно и то же трижды — значит и исправлять одну ошибку трижды. Класс умеет взять общее у другого класса, и тогда общее пишут один раз:

SWIFT
class Animal {
    var name: String
    init(name: String) {
        self.name = name
    }
    func speak() -> String { "..." }
}

class Dog: Animal {
    override func speak() -> String { "Woof" }
}

let rex = Dog(name: "Rex")
print(rex.name, rex.speak()) // Rex Woof

Давайте разберём программу:

  1. class Animal — общий класс: у него свойство name, init(name:) и метод speak().
  2. class Dog: Animal — после двоеточия стоит класс, от которого Dog берёт всё общее.
  3. В Dog нет ни name, ни своего init, но Dog(name: "Rex") работает: и то и другое Dog получил от Animal.
  4. override func speak() заменяет голос Animal своим, поэтому rex.speak() возвращает Woof.

Подкласс и родительский класс

Наследование — способ объявить класс на основе другого: новый класс получает все его свойства и методы. Класс, который наследует, называют подклассом, а класс, от которого наследуют, называют родительским. В примере Dog — подкласс, а Animal — его родительский класс.

Подкласс может добавить свои свойства и методы, а может заменить метод родителя своей версией. Такую замену называют переопределением, и перед ней пишут override.

Наследование есть только у классов: структуру от другой структуры унаследовать нельзя.

Здесь картинка с собакой неточна. Настоящая собака бывает сразу и животным, и питомцем, и сторожем, а у класса в Swift родительский класс только один.

Родительский тип и super

Подкласс годится везде, где ждут родительский класс: собака — тоже животное. Поэтому константа типа Animal может хранить экземпляр подкласса. А внутри переопределённого метода можно вызвать версию родителя через super:

SWIFT
class Puppy: Dog {
    override func speak() -> String {
        super.speak() + "!"
    }
}

let pet: Animal = Puppy(name: "Bim")
print(pet.speak()) // Woof!
print(type(of: pet)) // Puppy

Разберём, что здесь происходит:

  1. Puppy наследует от Dog, а значит, и от Animal.
  2. super.speak() вызывает версию родителя, то есть Dog, и получает Woof. Puppy добавляет к ней !.
  3. pet объявлена как Animal, но экземпляр в ней — Puppy. Вызов pet.speak() запускает версию Puppy: версию метода выбирает класс самого экземпляра, а не тип константы.
  4. type(of: pet) это подтверждает и печатает Puppy.

Вызов super выполняется ровно там, где его написали. Код переопределённого метода до этого вызова выполняется раньше версии родителя, а код после — позже.

На собеседовании часто спрашивают, чью версию метода запустит вызов, если константа объявлена с типом родительского класса. Ответ: версию класса самого экземпляра.

Частая ошибка

Метод с тем же именем, что у родителя, написали без override:

SWIFT
class Cat: Animal {
    func speak() -> String { "Meow" } // ✗
}
TEXT
error: overriding declaration requires an 'override' keyword

Программа не компилируется: метод speak() уже есть у Animal, и Swift требует явно сказать, что вы его заменяете. Допишите override перед func.

Обратная ошибка тоже ловится. Если написать override у метода, которого у родителя нет, например с опечаткой speek(), программа не скомпилируется. Так override защищает от двух случайностей: заменить метод родителя, не заметив этого, и промахнуться мимо него.

Главное

  • class Dog: Animal объявляет подкласс: он получает все свойства и методы родительского класса.
  • override заменяет метод родителя, а super.speak() вызывает версию родителя там, где его написали.
  • Константа родительского типа может хранить экземпляр подкласса, и вызов метода запускает версию класса самого экземпляра.
  • override обязателен у метода, который заменяет метод родителя, и запрещён у метода, которого у родителя нет.