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

Класс может наследовать методы, свойства и другие характеристики другого класса. Когда один класс наследует у другого класса, то наследующий класс называется подклассом, класс у которого наследуют - суперклассом. Наследование - фундаментальное поведение, которое отделяет классы от других типов Swift.

17 ноября 2022

 

Классы в Swift могут вызывать или получать доступ к методам, свойствам, индексам, принадлежащим их суперклассам и могут предоставлять свои собственные переписанные версии этих методов, свойств, индексов для усовершенствования или изменения их поведения.

Классы так же могут добавлять наблюдателей свойств к наследованным свойствам для того, чтобы быть в курсе, когда происходит смена значения свойства. Наблюдатели свойств могут быть добавлены для любого свойства, несмотря на то были ли они изначально определены как хранимые свойства или вычисляемые.

Определение базового класса

Любой класс, который ничего не наследует из другого класса, называется базовым классом.

Заметка

Классы в Swift ничего не наследуют от универсального базового класса. Классы, у которых не указан супер класс (родительский класс), называются базовыми, которые вы можете использовать для строительства других классов.

Пример ниже определяет класс Vehicle. Этот базовый класс определяет хранимое свойство currentSpeed, с начальным значением 0.0 (выведенный тип Double). Значение свойства currentSpeed используется вычисляемым нередактируемым свойством description типа String, для создания описания транспортного средства (экземпляра Vehicle).

Так же класс Vehicle определяет метод makeNoise. Этот метод фактически ничего не делает для базового экземпляра класса Vehicle, но будет настраиваться подклассом класса Vehicle чуть позже:


class Vehicle {
  var currentSpeed = 0.0
  var description: String {
    return "движется на скорости \(currentSpeed) миль в час"
  }
  func makeNoise() {
    //ничего не делаем, так как не каждый транспорт шумит
  }
}

Вы создаете новый экземпляр класса Vehicle при помощи синтаксиса инициализатора, который написан как TypeName, за которым идут пустые круглые скобки:


let someVehicle = Vehicle()

Создав новый экземпляр класса Vehicle, вы можете получить доступ к его свойству description, для вывода на экран описания текущей скорости транспорта:


print("Транспорт: \(someVehicle.description)")
//Транспорт: движется на скорости 0.0 миль в час

Класс Vehicle определяет обычные характеристики для обычного транспортного средства, но особо мы их использовать не можем. Чтобы сделать класс более полезным, вам нужно усовершенствовать его для описания более специфичных видов транспорта.

Наследование подклассом

Наследование является актом создания нового класса на базе существующего класса (базового класса). Подкласс наследует характеристики от существующего класса, который затем может быть усовершенствован. Вы так же можете добавить новые характеристики подклассу.

Для индикации того, что подкласс имеет суперкласс, просто напишите имя подкласса, затем имя суперкласса и разделите их двоеточием:


class SomeSubclass: SomeSuperclass {
  // определение подкласса проводится тут
}

Приведенный пример определяет подкласс Bicycle с суперклассом Vehicle:


class Bicycle: Vehicle {
  var hasBasket = false
}

Новый класс Bicycle автоматически собирает все характеристики Vehicle, например, такие свойства как currentSpeed и description и метод makeNoise().

В дополнение к характеристикам, которые он наследует, класс Bicycle определяет свое новое хранимое свойство hasBasket, со значением по умолчанию false (тип свойства выведен как Bool).

По умолчанию, любой новый экземпляр Bicycle, который вы создадите не будет иметь корзину (hasBasket = false). Вы можете установить hasBasket на значение true для конкретного экземпляра Bicycle, после того как он создан:


let bicycle = Bicycle()
bicycle.hasBasket = true

Вы так же можете изменить унаследованное свойство currentSpeed экземпляра Bicycle и запросить его свойство description:


bicycle.currentSpeed = 15.0
print("Велосипед: \(bicycle.description)")
//Велосипед: движется на скорости 15.0 миль в час

Подклассы сами могут создавать подклассы. В следующем примере класс Bicycle создает подкласс для двухместного велосипеда известного как “тандем”:


class Tandem: Bicycle {
  var currentNumberOfPassengers = 0
}

Класс Tandem наследует все свойства и методы Bicycle, который в свою очередь наследует все свойства и методы от Vehicle. Подкласс Tandem так же добавляет новое хранимое свойство currentNumberOfPassengers, которое по умолчанию равно 0.

Если вы создадите экземпляр Tandem, то вы можете работать с любым из его новых и унаследованных свойств. Свойство description, которое является свойством только для чтения, он наследует от Vehicle:


let tandem = Tandem()
tandem.hasBasket = true
tandem.currentNumberOfPassengers = 2
tandem.currentSpeed = 22.0
print("Тандем: \(tandem.description)")
// Тандем: движется на скорости 22.0 миль в час

Переопределение

Подклассы могут проводить свои собственные реализации методов экземпляра, методов класса, свойств экземпляра, свойств класса или индекса, который в противном случае будет наследовать от суперкласса. Это известно как переопределение.

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

Ключевое слово override так же подсказывает компилятору Swift проверить, что вы переопределяете суперкласс класса (или один из его параметров), который содержит то определение, которое вы хотите переопределить. Эта проверка гарантирует, что ваше переопределение корректно.

Доступ к методам, свойствам, индексам суперкласса

Когда вы проводите переопределение метода, свойства, индекса для подкласса, иногда бывает полезно использовать существующую реализацию суперкласса как часть вашего переопределения. Для примера, вы можете усовершенствовать поведение существующей реализации или сохранить измененное значение в существующей унаследованной переменной.

Там, где это уместно, вы можете получить доступ к методу, свойству, индексу версии суперкласса, если будете использовать префикс super:

  • Переопределенный метод someMethod может вызвать версию суперкласса метода someMethod, написав super.someMethod() внутри переопределения реализации метода.
  • Переопределённое свойство someProperty может получить доступ к свойству версии суперкласса someProperty как super.someProperty внутри переопределения реализации геттера или сеттера.
  • Переопределенный индекс для someIndex может получить доступ к версии суперкласса того же индекса как super[someIndex] изнутри переопределения реализации индекса.

Переопределение методов

Вы можете переопределить унаследованный метод экземпляра или класса, чтобы обеспечить индивидуальную или альтернативную версию реализации метода в подклассе.

Следующий пример определяет новый подкласс Train класса Vehicle, который переопределяет метод makeNoise(), который Train наследует от Vehicle:


class Train: Vehicle {
  override func makeNoise() {
    print("Чу-чу")
  }
}

Если вы создаете новый экземпляр класса Train и вызовите его метод makeNoise(), вы увидите, что версия метода подкласса Train вызывается вот так:


let train = Train()
train.makeNoise()
// Выведет "Чу-чу"

Переопределение свойств

Вы можете переопределить унаследованные свойства класса или экземпляра для установки вашего собственного геттера и сеттера для этого свойства, или добавить наблюдателя свойства для наблюдения за переопределяемым свойством, когда меняется лежащее в основе значение свойства.

Переопределения геттеров и сеттеров свойства

Вы можете предусмотреть пользовательский геттер (и сеттер, если есть в этом необходимость) для переопределения любого унаследованного свойства, несмотря на то, как свойство было определено в самом источнике, как свойство хранения или как вычисляемое. Подкласс не знает каким является унаследованное свойство хранимым или вычисляемым, все что он знает, так это имя свойства и его тип. Вы всегда должны констатировать и имя, и тип свойства, которое вы переопределяете, для того чтобы компилятор мог проверить соответствие и наличие переопределяемого свойства у суперкласса.

Вы можете представить унаследованное свойство только для чтения, как свойство, которое можно читать и редактировать, прописывая и геттер и сеттер в вашем переопределяемом свойстве подкласса. Однако вы не можете сделать наоборот, то есть сделать свойство редактируемое и читаемое только свойством для чтения.

Заметка

Если вы предоставляете сеттер как часть переопределения свойства, то вы должны предоставить и геттер для этого переопределения. Если вы не хотите изменять значение наследуемого свойства внутри переопределяемого геттера, то вы можете просто передать через наследуемое значение, возвращая super.someProperty от геттера, где someProperty - имя параметра, который вы переопределяете.

Следующий пример определяет класс Car, который является подклассом Vehicle. Класс Car предоставляет новое свойство хранения gear, имеющее значение по умолчанию равное 1. Класс Car так же переопределяет свойство description, которое он унаследовал от Vehicle, для предоставления собственного описания, которое включает в себя текущую передачу:


class Car: Vehicle {
  var gear = 1
  override var description: String {
    return super.description + " на передаче \(gear)"
  }
}

Переопределение свойства description начинается с super.description, который возвращает свойство description класса Vehicle. Версия класса Car свойства description добавляет дополнительный текст в конец описания текущего свойства description.

Если вы создадите экземпляр класса Car и зададите свойства gear, currentSpeed, то вы увидите что его свойство description возвращает новое описание класса Car:


let car = Car()
car.currentSpeed = 25.0
car.gear = 3
print("Машина: \(car.description)")
// Выведет "Машина: движется на скорости 25.0 миль в час на передаче 3"

Переопределение наблюдателей свойства

Вы можете использовать переопределение свойства для добавления наблюдателей к унаследованному свойству. Это позволяет вам получать уведомления об изменении значения унаследованного свойства, несмотря на то, как изначально это свойство было реализовано. Для большей информации о наблюдателях свойств читайте в главе Наблюдатели свойств.

Заметка

Вы не можете добавить наблюдателей свойства на унаследованное константное свойство или на унаследованные вычисляемые свойства только для чтения. Значение этих свойств не может меняться, так что нет никакого смысла вписывать willSet, didSet как часть их реализации.

Также заметим, что вы не можете обеспечить одно и то же свойство и переопределяемым наблюдателем, и переопределяемым сеттером. Если вы хотите наблюдать за изменениями значения свойства, и вы готовы предоставить пользовательский сеттер для этого свойства, то вы можете просто наблюдать за изменением какого-либо значения из сеттера.

В следующем примере определим новый класс AutomaticCar, который является подклассом Car. Класс AutomaticCar представляет машину с автоматической коробкой передач, которая автоматически переключает передачи в зависимости от текущей скорости:


class AutomaticCar: Car {
  override var currentSpeed: Double {
    didSet {
      gear = Int(currentSpeed / 10.0) + 1
    }
  }
}

Куда бы вы не поставили свойство currentSpeed экземпляра класса AutomaticCar, наблюдатель didSet свойства устанавливает свойство экземпляра gear в подходящее значение передачи в зависимости от скорости. Если быть точным, то наблюдатель свойства выбирает передачу как значение равное currentSpeed поделенная на 10 и округленная вниз и выбираем ближайшее целое число + 1. Если скорость равна 10.0, то передача равна 2, если скорость 35.0, то передача 4:


let automatic = AutomaticCar()
automatic.currentSpeed = 35.0
print("Машина с автоматом: \(automatic.description)")
//Выведет "Машина с автоматом: движется на скорости 35.0 миль в час на передаче 4"

Предотвращение переопределений

Вы можете предотвратить переопределение метода, свойства или индекса, обозначив его как конечный. Сделать это можно написав ключевое слово final перед ключевым словом метода, свойства или индекса (final var, final func, final class func, и final subscript ).

Любая попытка переписать конечный метод, свойство или индекс в подклассе приведет к ошибке компиляции. Методы, свойства и индексы, которые вы добавляете в класс в расширении, так же могут быть отмечены как конечные внутри определения расширения.

Вы можете отметить целый класс как конечный или финальный, написав слово final перед ключевым словом class (final class). Любая попытка унаследовать класс также приведет к ошибке компиляции.

Содержание