Наследование
Собака — это животное. Всё, что есть у любого животного, есть и у собаки: например, кличка и голос. Но голос у собаки особый — «гав».
Теперь представьте программу с классами Dog, Cat и Parrot, и у каждого — кличка, init и метод speak(). Писать одно и то же трижды — значит и исправлять одну ошибку трижды. Класс умеет взять общее у другого класса, и тогда общее пишут один раз:
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Давайте разберём программу:
class Animal— общий класс: у него свойствоname,init(name:)и методspeak().class Dog: Animal— после двоеточия стоит класс, от которогоDogберёт всё общее.- В
Dogнет ниname, ни своегоinit, ноDog(name: "Rex")работает: и то и другоеDogполучил отAnimal. override func speak()заменяет голосAnimalсвоим, поэтомуrex.speak()возвращаетWoof.
Подкласс и родительский класс
Наследование — способ объявить класс на основе другого: новый класс получает все его свойства и методы. Класс, который наследует, называют подклассом, а класс, от которого наследуют, называют родительским. В примере Dog — подкласс, а Animal — его родительский класс.
Подкласс может добавить свои свойства и методы, а может заменить метод родителя своей версией. Такую замену называют переопределением, и перед ней пишут override.
Наследование есть только у классов: структуру от другой структуры унаследовать нельзя.
Здесь картинка с собакой неточна. Настоящая собака бывает сразу и животным, и питомцем, и сторожем, а у класса в Swift родительский класс только один.
Родительский тип и super
Подкласс годится везде, где ждут родительский класс: собака — тоже животное. Поэтому константа типа Animal может хранить экземпляр подкласса. А внутри переопределённого метода можно вызвать версию родителя через super:
class Puppy: Dog {
override func speak() -> String {
super.speak() + "!"
}
}
let pet: Animal = Puppy(name: "Bim")
print(pet.speak()) // Woof!
print(type(of: pet)) // PuppyРазберём, что здесь происходит:
Puppyнаследует отDog, а значит, и отAnimal.super.speak()вызывает версию родителя, то естьDog, и получаетWoof.Puppyдобавляет к ней!.petобъявлена какAnimal, но экземпляр в ней —Puppy. Вызовpet.speak()запускает версиюPuppy: версию метода выбирает класс самого экземпляра, а не тип константы.type(of: pet)это подтверждает и печатаетPuppy.
Вызов super выполняется ровно там, где его написали. Код переопределённого метода до этого вызова выполняется раньше версии родителя, а код после — позже.
На собеседовании часто спрашивают, чью версию метода запустит вызов, если константа объявлена с типом родительского класса. Ответ: версию класса самого экземпляра.
Частая ошибка
Метод с тем же именем, что у родителя, написали без override:
class Cat: Animal {
func speak() -> String { "Meow" } // ✗
}error: overriding declaration requires an 'override' keywordПрограмма не компилируется: метод speak() уже есть у Animal, и Swift требует явно сказать, что вы его заменяете. Допишите override перед func.
Обратная ошибка тоже ловится. Если написать override у метода, которого у родителя нет, например с опечаткой speek(), программа не скомпилируется. Так override защищает от двух случайностей: заменить метод родителя, не заметив этого, и промахнуться мимо него.
Главное
class Dog: Animalобъявляет подкласс: он получает все свойства и методы родительского класса.overrideзаменяет метод родителя, аsuper.speak()вызывает версию родителя там, где его написали.- Константа родительского типа может хранить экземпляр подкласса, и вызов метода запускает версию класса самого экземпляра.
overrideобязателен у метода, который заменяет метод родителя, и запрещён у метода, которого у родителя нет.