Книга: Swift. Основы разработки приложений под iOS, iPadOS и macOS. 5-е изд. дополненное и переработанное
Назад: 26. Сабскрипты
Дальше: 28. Псевдонимы Any и AnyObject

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

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

В рамках наследования «старый» класс называется суперклассом (или базовым классом), а «новый» — подклассом (или субклассом, или производным классом).

27.1. Синтаксис наследования

Для наследования одного класса другим необходимо указать имя суперкласса через двоеточие после имени объявляемого класса.

Синтаксис

class SuperClass {

    // тело суперкласса

}

class SubClass: SuperClass {

    // тело подкласса

}

Для создания производного класса SubClass, для которого базовым является SuperClass, необходимо указать имя суперкласса через двоеточие после имени подкласса.

В результате все свойства и методы, определенные в классе SuperClass, становятся доступными в классе SubClass без их непосредственного объявления в производном типе.

ПРИМЕЧАНИЕ Значения наследуемых свойств могут изменяться независимо от значений соответствующих свойств родительского класса.

Рассмотрим пример, в котором создается базовый класс Quadruped с набором свойств и методов (листинг 27.1). Данный класс описывает сущность «четвероногое животное». Дополнительно объявляется субкласс Dog, описывающий сущность «собака». Все характеристики класса Quadruped применимы и к классу Dog, поэтому их можно наследовать.

Листинг 27.1

// суперкласс

class Quadruped {

    var type = ""

    var name = ""

    func walk(){

        print("walk")

    }

}

// подкласс

class Dog: Quadruped {

    func bark(){

        print("woof")

    }

}

var dog = Dog()

dog.type = "dog"

dog.walk() // выводит walk

dog.bark() // выводит woof

Экземпляр myDog позволяет получить доступ к свойствам и методам родительского класса Quadruped. Кроме того, класс Dog расширяет собственные возможности, реализуя в своем теле дополнительный метод bark().

ПРИМЕЧАНИЕ Класс может быть суперклассом для произвольного количества субклассов.

Доступ к наследуемым характеристикам

Доступ к наследуемым элементам родительского класса в произ­водном классе реализуется так же, как к собственным элементам данного производного класса, то есть с использованием ключевого слова self. В качестве примера в класс Dog добавим метод, выводящий на консоль кличку собаки. Кличка хранится в свойстве name, которое наследуется от класса Quadruped (листинг 27.2).

Листинг 27.2

class Dog: Quadruped {

    func bark(){

        print("woof")

    }

    // метод из листинга 2

    func printName(){

        print(self.name)

    }

}

var dog = Dog()

dog.name = "Dragon Wan Helsing"

dog.printName()

Консоль

Dragon Wan Helsing

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

27.2. Переопределение наследуемых элементов

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

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

Довольно часто реализация метода, который «достался в наследство» от суперкласса, не соответствует требованиям разработчика. В таком случае в субклассе нужно переписать данный метод, обеспечив к нему доступ по прежнему имени. Объявим новый класс NoisyDog, который описывает сущность «беспокойная собака». Класс Dog является суперклассом для NoisyDog. В описываемый класс необходимо внедрить собственную реализацию метода bark(), но так как одноименный метод уже существует в родительском классе Dog, мы воспользуемся механизмом переопределения (листинг 27.3).

Листинг 27.3

class NoisyDog: Dog{

    override func bark(){

        print ("woof")

        print ("woof")

        print ("woof")

    }

}

var badDog = NoisyDog()

badDog.bark()

Консоль

woof

woof

woof

С помощью ключевого слова override мы сообщаем Swift, что метод bark() в классе NoisyDog имеет собственную реализацию.

ПРИМЕЧАНИЕ Класс может быть суперклассом вне зависимости от того, является он субклассом или нет.

Переопределенный метод не знает деталей реализации метода родительского класса. Он знает лишь имя и перечень входных параметров родительского метода.

Доступ к переопределенным элементам суперкласса

Несмотря на то что переопределение изменяет реализацию свойств, методов и сабскриптов, Swift позволяет осуществлять доступ внутри производного класса к переопределенным элементам суперкласса. Для этого в качестве префикса имени элемента вместо self используется ключевое слово super.

В предыдущем примере в методе bark() класса NoisyDog происходит дублирование кода. В нем используется функция вывода на консоль литерала "woof", хотя данный функционал уже реализован в одноименном родительском методе. Перепишем реализацию метода bark() таким образом, чтобы избежать дублирования кода (листинг 27.4).

Листинг 27.4

class NoisyDog: Dog{

    override func bark(){

        for _ in 1...3 {

            super.bark()

        }

    }

}

var badDog = NoisyDog()

badDog.bark()

Консоль

woof

woof

woof

Вывод на консоль соответствует выводу реализации класса из предыдущего примера.

Доступ к переопределенным элементам осуществляется по следующим правилам:

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

• Переопределенное свойство someProperty может получить доступ к свойству суперкласса с таким же именем, используя конструкцию super.someProperty внутри реализации своего геттера или сеттера.

• Переопределенный сабскрипт someIndex может обратиться к сабскрипту суперкласса с таким же форматом индекса, используя конструкцию super[someIndex] внутри реализации сабскрипта.

Переопределение инициализаторов

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

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

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

Внимание Для вызова инициализатора суперкласса внутри инициализатора субкласса необходимо использовать конструкцию super.init().

В качестве примера переопределим наследуемый от суперкласса Quadruped пустой инициализатор. В классе Dog значение наследуемого свойства type всегда должно быть равно "dog". В связи с этим перепишем реализацию инициализатора таким образом, чтобы в нем устанавливалось значение данного свойства (листинг 27.5).

Листинг 27.5

class Dog: Quadruped {

    override init(){

        super.init()

        self.type = "dog"

    }

    func bark(){

        print("woof")

    }

    func printName(){

        print(self.name)

    }

}

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

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

Переопределение наследуемых свойств

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

С помощью механизма переопределения вы можете расширить наследуемое «только для чтения» свойство до «чтение-запись», реализовав в нем и геттер и сеттер. Но обратное невозможно: если у наследуемого свойства реализованы и геттер и сеттер, вы не сможете сделать из него свойство «только для чтения».

ПРИМЕЧАНИЕ Хранимые свойства переопределять нельзя, так как вызываемый или наследуемый инициализатор родительского класса попытается установить их значения, но не найдет их.

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

27.3. Превентивный модификатор final

Swift позволяет защитить реализацию класса целиком или его отдельных элементов. Для этого необходимо использовать превентивный модификатор final, который указывается перед объявлением класса или его отдельных элементов:

• final class для классов;

• final var для свойств;

• final func для методов;

• final subscript для сабскриптов.

При защите реализации класса его наследование в другие классы становится невозможным. Для элементов класса их наследование происходит, но переопределение становится недоступным.

27.4. Подмена экземпляров классов

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

Рассмотрим пример из листинга 27.6. В нем объявим массив элементов типа Quadruped и добавим в него несколько элементов.

Листинг 27.6

var animalsArray: [Quadruped] = []

var someAnimal = Quadruped()

var myDog = Dog()

var sadDog = NoisyDog()

animalsArray.append(someAnimal)

animalsArray.append(myDog)

animalsArray.append(sadDog)

В результате в массив animalsArray добавляются элементы типов Dog и NoisyDog. Это происходит несмотря на то, что в качестве типа массива указан класс Quadruped.

27.5. Приведение типов

Ранее нами были созданы три класса: Quadruped, Dog и NoisyDog, а также определен массив animalsArray, содержащий элементы всех трех типов данных. Данный набор типов представляет собой иерархию классов, поскольку между всеми классами можно указать четкие зависимости (кто кого наследует). Для анализа классов в единой иерархии существует специальный механизм, называемый приведением типов.

Путем приведения типов вы можете выполнить следующие операции:

• проверить тип конкретного экземпляра класса на соответствие некоторому типу или протоколу;

• преобразовать тип конкретного экземпляра в другой тип той же иерархии классов.

Проверка типа

Проверка типа экземпляра класса производится с помощью оператора is. Данный оператор возвращает true в случае, когда тип проверяемого экземпляра является указанным после оператора классом или наследует его. Для анализа возьмем определенный и заполненный ранее массив animalsArray (листинг 27.7).

Листинг 27.7

for item in animalsArray {

    if item is Dog {

        print("Yap")

    }

}

// Yap выводится 2 раза

Данный код перебирает все элементы массива animalsArray и проверяет их на соответствие классу Dog. В результате выясняется, что ему соответствуют только два элемента массива: экземпляр класса Dog и экземпляр класса NoisyDog.

Преобразование типа

Как отмечалось ранее, массив animalsArray имеет элементы разных типов данных из одной иерархической структуры. Несмотря на это, при получении очередного элемента вы будете работать исключительно с использованием методов класса, указанного в типе массива (в данном случае Quadruped). То есть, получив элемент типа Dog, вы не увидите определенный в нем метод bark(), поскольку Swift подра­зумевает, что вы работаете именно с экземпляром типа Quadruped.

Для того чтобы преобразовать тип и сообщить Swift, что данный элемент является экземпляром определенного типа, используется оператор as, точнее, две его вариации: as? и as!. Данный оператор ставится после имени экземпляра, а после него указывается имя класса, в который преобразуется экземпляр.

Между обеими формами оператора существует разница:

• as? ИмяКласса возвращает либо экземпляр типа ИмяКласса? (опционал), либо nil в случае неудачного преобразования;

• as! ИмяКласса производит принудительное извлечение значения и возвращает экземпляр типа ИмяКласса или вызывает ошибку в случае неудачи.

Внимание Тип данных может быть преобразован только в пределах собственной иерархии классов.

Снова приступим к перебору массива animalsArray. На этот раз будем вызывать метод bark(), который не существует в суперклассе Quadruped, но присутствует в подклассах Dog и NoisyDog (листинг 27.8).

Листинг 27.8

for item in animalsArray {

    if var animal = item as? NoisyDog {

        animal.bark()

    }else if var animal = item as? Dog {

        animal.bark()

    }else{

        item.walk()

    }

}

Каждый элемент массива animalArray связывается с параметром item. Далее в теле цикла данный параметр с использованием оператора as? пытается преобразоваться в каждый из типов данных нашей структуры классов. Если item преобразуется в тип NoisyDog или Dog, то у него становится доступным метод bark().

Назад: 26. Сабскрипты
Дальше: 28. Псевдонимы Any и AnyObject