Видеокурсы по изучению языка программирования Swift. Подробнее

Наследование и инициализация класса

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

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

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

Назначенный и вспомогательный инициализатор

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

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

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

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

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

Синтаксис назначенных и вспомогательных инициализаторов

Назначенные инициализаторы для классов записываются точно так же как и простые инициализаторы для типов значений:

init(параметры) {
     выражения
}

Вспомогательные инициализаторы пишутся точно так же, но только дополнительно используется вспомогательное слово convenience, которое располагается до слова init и разделяется пробелом:

convenience init(параметры) {
     выражения
}

Делегирование инициализатора для классовых типов

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

Правило 1

Назначенный инициализатор должен вызывать назначенный инициализатор из суперкласса.

Правило 2

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

Правило 3

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

Вот как можно просто это запомнить:

  • Назначенные инициализаторы должны делегировать наверх
  • Вспомогательные инициализаторы должны делегировать по своему уровню (классу).

Вот как это правило выглядит в иллюстрированной форме:

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

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

Заметка

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

Схема ниже показывает более сложную иерархию из четырех классов. Она показывает как назначенные инициализаторы работают в качестве точек прохождения инициализации класса, упрощая внутренние взаимоотношения среди цепочки классов:

Двухфазная инициализация

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

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

Заметка

Двухфазный процесс инициализации в Swift аналогичен инициализации в Objective-C. Основное отличие между ними проходит на первой фазе в том, что в Objective-C свойства получают значения 0 или nil. В Swift же этот процесс более гибкий и позволяет устанавливать пользовательские начальные значения и может обработать типы, для которых значения 0 или nil, являются некорректными.

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

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

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

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

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

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

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

Вот как проходит двухфазная инициализация, основанная на четырех проверках(описанные выше):

Фаза первая

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

Фаза вторая

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

Вот как выглядит первая фаза для гипотетического подкласса и суперкласса:

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

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

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

Сразу после того как все свойства суперкласса получают начальные значения, память считается полностью инициализированной, Фаза 1 завершается.

Вот как выглядит Фаза 2:

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

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

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

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

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

В отличии от подклассов в Objective-C, подклассы в Swift не наследуют инициализаторов их суперклассов по умолчанию. Такой подход в Swift предотвращает ситуации, когда простой инициализатор суперкласса наследуется более специфичным подклассом, а потом используется для создания экземпляра подкласса, который не полностью или не правильно инициализирован.

Заметка

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

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

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

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

Заметка

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

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

Пример ниже определяет базовый класс Vehicle. Это базовый класс объявляет свойства numberOfWheels со значением 0 типа Int. Свойство numberOfWheels используется для вычисляемого свойства description, для создания описания характеристик транспортного средства типа String:

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

Класс Vehicle предоставляет значение по умолчанию для его единственного свойства, и не имеет никаких собственных пользовательских инициализаторов. И в результате он автоматически получает дефолтный инициализатор, как описано в главе Дефолтные инициализаторы. Дефолтный инициализатор (когда доступен) всегда является назначенным инициализатором для класса и может быть использован для создания нового экземпляра класса Vehicle с numberOfWheels равным 0:

let vehicle = Vehicle()
print("Транспортное средство \(vehicle.description)")
//Транспортное средство 0 колес(о)

Следующий пример определяет подкласс Bicycle суперкласса Vehicle:

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

Подкласс Bicycle определяет пользовательский назначенный инициализатор init(). Назначенный инициализатор совпадает с назначенным инициализатором из суперкласса Bicycle и, таким образом, версия этого инициализатора класса Bicycle отмечена модификатором override.

Инициализатор init() для Bicycle начинается с вызова super.init(), который в свою очередь вызывает дефолтный инициализатор для суперкласса Vehicle класса Bicycle. Он проверяет, что унаследованное свойство numberOfWheels инициализировано в Vehicle, после чего у Bicycle появляется возможность его модифицировать. После вызова super.init() начальное значение numberOfWheels заменяется значением 2.

Если вы создаете экземпляр Bicycle, вы можете вызвать его унаследованное вычисляемое свойство description, для того, чтобы посмотреть как обновилось свойство numberOfWheels:

let bicycle = Bicycle()
print("Велосипед: \(bicycle.description)")
//Велосипед: 2 колес(а)

Заметка

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

Автоматическое наследование инициализатора

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

Допуская, что вы предоставляете значения по умолчанию любому новому свойству, представленному в подклассе, то применяются два правила:

Правило 1. Если ваш подкласс не определяет ни одного назначенного инициализатора, он автоматически наследует все назначенные инициализаторы суперкласса.

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

Эти правила применимы даже если ваш подкласс позже добавляет вспомогательные инициализаторы.

Заметка

Подкласс может реализовать назначенный инициализатор суперкласса как вспомогательный инициализатор подкласса в качестве части удовлетворяющей правилу 2.

Назначенные и вспомогательные инициализаторы в действии

Следующий пример показывает назначенные и вспомогательные инициализаторы, и автоматическое наследование инициализатора в действии. Этот пример определяет иерархию трех классов Food, RecipeIngredient и ShoppingListItem и демонстрирует как их инициализаторы взаимодействуют.

Основной (базовый) класс называется Food, который имеет одно простое свойство типа String называемое name и обеспечивает два инициализатора для создания экземпляров класса Food:

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

Схема ниже показывает цепочку работы инициализаторов в классе Food :

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

let namedMeat = Food(name: "Бекон")
//имя namedMeat является "Бекон"

Инициализатор init(name: String) из класса Food, представлен в виде назначенного инициализатора, потому что он проверяет, что все хранимые свойства нового экземпляра Food полностью инициализированы. Класс Food не имеет суперкласса, так что инициализатор init(name: String) не имеет вызова super.init() для завершения своей инициализации.

Класс Food так же обеспечивает вспомогательный инициализатор init() без аргументов. Инициализатор init() предоставляет имя плейсхолдера для новой еды, делегируя к параметру name инициализатора init(name: String), давая ему значение [Unnamed] :

let mysteryMeat = Food()
//mysteryMeat называется "[Unnamed]"

Второй класс в иерархии - это подкласс RecipeIngredient класса Food. Класс RecipeIngredient создает модель ингредиентов в рецепте. Он представляет свойство quantity типа Int (в дополнение к свойству name, унаследованное от Food) и определяет два инициализатора для создания экземпляров RecipeIngredient :

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

Схема ниже показывает цепочку инициализаторов для класса RecipeIngredient:

Класс RecipeIngredient имеет один назначенный инициализатор init(name: String, quantity: Int), который может быть использован для распространения всех свойств нового экземпляра RecipeIngredient. Этот инициализатор начинается с присваивания переданного аргумента quantity свойству quantity, которое является единственным новым свойством представленным в RecipeIngredient. После того как это сделано, инициализатор делегирует вверх инициализатор init(name: String) для класса Food. Этот процесс удовлетворяет проверке №1 из раздела “Двухфазная инициализация”, что находится выше на этой же странице.

RecipeIngredient так же определяет вспомогательный инициализатор init(name: String), который используется создания экземпляра RecipeIngredient только по имени. Этот вспомогательный инициализатор присваивает значение количество равное 1 для любого экземпляра, которое создано без явного указания количества. Определение этого вспомогательного инициализатора ускоряет создание экземпляров класса RecipeIngredient и позволяет избежать повторения кода при создании экземпляра, где свойство quantity изначально всегда равно 1. Этот вспомогательный инициализатор делегирует по назначенному инициализатору класса, передавая ему quantity равное 1.

Вспомогательный инициализатор init(name: String) предоставленный RecipeIngredient’ом принимает те же параметры, что и назначенный инициализатор init(name: String) в Food. Из-за того, что вспомогательный инициализатор переопределяет назначенный инициализатор из своего суперкласса, то он должен быть обозначен ключевым словом override.

Даже если RecipeIngredient представляет инициализатор init(name: String) как вспомогательный инициализатор, то RecipeIngredient тем не менее проводит реализацию всех назначенных инициализаторов своего суперкласса. Таким образом RecipeIngredient автоматически наследует все свойства вспомогательных инициализаторов своего суперкласса тоже.

В этом примере суперкласс для класса RecipeIngredient является Food, который имеет единственный инициализатор init(). Поэтому этот инициализатор наследуется RecipeIngredient. Наследованная версия init() функционирует абсолютно так же как и версия в Food, за исключением того, что она делегирует в RecipeIngredient версию init(name: String), а не в версию Food.

Все три инициализатора могут быть использованы для создания новых RecipeIngredient экземпляров:

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

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

Каждый элемент в листе покупок (shopping list) начинается с “не куплен” или “unpurchased”. Для отображения того факта, что ShoppingListItem представляет булево свойство purchased, со значением по умолчанию false. ShoppingListItem так же добавляет высчитываемое свойство description, которое предоставляет текстовое описание экземпляра ShoppingListItem :

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

Заметка

ShoppingListItem не определяет инициализатор для предоставления исходного значения для purchased, потому что элементы в листе покупок, как смоделировано тут, сначала имеют значение false, то есть они не куплены.

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

Схема ниже отображает общую цепочку инициализаций для всех трех классов:

Вы можете использовать все три унаследованных инициализатора для создания нового экземпляра ShoppingListItem :

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Bacon"),
    ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
    print(item.description)
}
// 1 x Orange juice ✔
// 1 x Bacon ✘
// 6 x Eggs ✘

Здесь мы создаем новый массив breakfastList, заполняя его тремя экземплярами класса ShoppingListItem. Тип массива выводится из [ShoppingListItem]. После того как массив создан, мы меняем исходное имя с "[Unnamed]" на "Orange juice и присваиваем свойству purchased значение true. Потом мы выводим на экран описание каждого элемента массива, где мы можем видеть, что все начальные значения установлены так как мы и ожидали.

 

Swift: 
4.0