Основы Swift / 13.2. Делегация инициализаторов


Видео


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

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

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

Заметьте, что если Вы определяете собственный инициализатор для типа-значения, то Вы больше не будете иметь доступа к дефолтному конструктору (или почленному конструктору, если речь идёт о структуре) для этого типа. Это ограничение предотвращает ситуацию, в которой дополнительная установка предоставляется в более сложно инициализаторе, который по ошибке перепутан с одним из автоматических.
@{13.2\1\1}
Следующий пример определяет произвольную структуру Rect для представления геометрического прямоугольника. Пример требует использования двух поддерживающих структур с названиями Size и Point, каждая из которых представляет значения по-умолчанию 0.0 для всех своих свойств:
struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}
@{13.2\1\2}
Вы можете инициализировать структуру Rect одним из трёх способов - использованием нуль-заданных значений свойств origin и size, предоставлением специального размера и точки начала или предоставлением специальной центровой точки и размера. Эти варианты инициализации представлены тремя инициализаторами, которые являются частью определения структуры Rect:
struct Rect {
    var origin = Point()
    var size = Size()
    init() {}
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}
@{13.2\1\3}
Первый конструктор типа Rect - init() - функционально идентичен тому, что получить структура по-умолчанию, если не будет иметь своих собственных конструкторов. Этот конструктор имеет пустое тело, что представляется пустой парой фигурных скобок {}. Вызов этого конструктора вернёт объект типа Rect, чьи свойства origin и size оба имеют значения по-умолчанию: Point(x: 0.0, y: 0.0) и Size(width: 0.0, height: 0.0) из их определений свойств:
let basicRect = Rect()
// начало координат basicRect равно (0.0, 0.0) и его размеры равны (0.0, 0.0)
@{13.2\1\4}
Второй конструктор init(origin:size:) функционально идентичен конструктору почленному, который структура получила бы, если бы не имела своих собственных конструкторов. Этот конструктор просто присваивает значения аргументов origin и size соответствующим хранимым свойствам:
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                      size: Size(width: 5.0, height: 5.0))
// начало координат originRect равно (2.0, 2.0) и его размеры (5.0, 5.0)
@{13.2\1\5}
Третий инициализатор init(center:size:) более сложен. Он начинается с вычисления подходящего центра координат, основываясь на точке center и значении size. Затем он вызывает (или делегирует) управление конструктору init(origin:size:), который сохраняет новые значения центра координат и размеров в соответствующие свойства:
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))
// начало координат centerRect равно (2.5, 2.5) и его размеры равны (3.0, 3.0)
Конструктор init(center:size:) мог бы самостоятельно присвоить новые значения origin и size соответствующим свойствам. Однако более удобно (и более просто для понимания) для конструктора init(center:size:) воспользоваться преимуществами существующего конструктора, который уже предлагает тот же функционал.
Наследование классов и инициализация
Все хранимые свойств класса, включая все свойства, которые класс наследует от надкласса, должны быть присвоены в процессе инициализации.

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

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

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

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

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

Вспомогательные инициализаторы записываются в том же стиле, но модификатором convenience перед ключевым словом init, разделённые пробелом.
Делегация инициализаторов для типов-классов
Правило 1:
Основной инициализатор должен вызвать основной инициализатор из своего непосредственного надкласса.

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

Правило 3:
Вспомогательный инициализатор должен обязательно вызвать основной инициализатор.

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

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

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

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

Рисунок демонстрирует более сложную иерархию класса для четырёх классов. Он иллюстрирует, как основные инициализаторы в этой иерархии работают в качестве туннельных точек для инициализации классов, упрощая взаимоотношения между классами в цепочке:
Рисунок 13.2.2
Двухфазная инициализация
Инициализация класса в Swift - это двухфазный процесс. На первой фазе каждое хранимое свойство получает изначальное значение от класса, который их вводит.

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

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

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

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

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

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

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

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

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

Здесь показано действие двухфазовой инициализации, основываясь на четырёх проверках безопасности выше:

Фаза 1:
  • Основной или вспомогательный инициализатор вызывается на классе.
  • Память для нового экземпляра класса аллоциируется. Память ещё не инициализирована.
  • Основной инициализатор для этого класса подтверждает, что все хранимые свойства, представленные этим классом имеют значение. Память для этих хранимых свистов теперь инициализируется.
  • Основной конструктор передаёт управление инициализатору надкласса для выполнения тех же задач для его хранимых свойств.
  • Это продолжается вверх по цепочке наследования, пока её вверх не будет достигнут.
  • Как только верх цепочки достигнут, а финальный класс в цепочке убедился, что все его хранимые свойства имеют значение, память для объекта полностью проинициализирована, а фаза 1 завершается.
Фаза 2:
  • Спускаясь обратно вниз по цепочке, каждый основной инициализатор в цепочке имеет возможность изменить объект далее. Теперь инициализаторы имеют возможность получить доступ к @code(self) и изменить его свойства, вызвать методы объекта и тому подобное.
  • В конце концов любой вспомогательный инициализатор в цепочке получает возможность изменить объект и работать с @code(self).
Здесь представлен пример того, как фаза 1 выглядит для инициализации вызова для гипотетического подкласса и надкласса:
Рисунок 13.2.3
Пример двухфазовой инициализации
В этом примере инициализация начинается с вызова вспомогательного инициализатора для подкласса. Этот вспомогательный инициализатор ещё не может изменить никакие свойства. Он делегирует в основной инициализатор того же класса.

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

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

Как только все свойства надкласса получают изначальное значение, то память полностью инициализирована, а фаза 1 завершается.
Рисунок 13.2.4
Пример двухфазовой инициализации 2
Основной инициализатор суперкласса теперь имеет возможность изменить объект далее (хотя он и не обязан это делать).

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

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

Наблюдатели willSet и didSet свойств надкласса вызываются, когда свойство устанавливается в конструкторе подкласса после вызова конструктора надкласса. Они не вызываются, когда класс устанавливает свои собственные свойства перед вызовом инициализатора суперкласса.
Наследование инициализаторов и перезапись
Подклассы в Swift не наследуют по-умолчанию инициализаторы своего надкласса. Подход Swift препятствует возникновению ситуации, когда простой инициализатор суперкласса насладится более усложнённым подклассом и используется для создания нового объекта подкласса, который не будет полностью или корректно инициализирован.

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

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

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

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

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

Напротив, если Вы создаёте инициализатор подкласса, который совпадает с вспомогательным инициализатором надкласса, то такой инициализатор не может быть напрямую вызван Вашим подклассом, что соответствует правилам. Следовательно, строго говоря, Ваш подкласс не предоставляет перезапись для инициализатора суперкласса. В результате Вам не нужно писать модификатор override, кода Вы предоставляете совпадающую реализацию для вспомогательного инициализатора надкласса.
@{13.2\2\1}
Пример ниже определяет базовый класс с названием Vehicle. Этот базовый класс определяет хранимое свойство с названием numberOfWheels со значением по-умолчанию типа Int, равным 0. Свойство numberOfWheels используется вычисляемым свойством description для создания описания типа String характеристик транспорта:
class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}
@{13.2\2\2]
Класс Vehicle предоставляет значение по-умолчанию для его единственного хранимого свойства и не предоставляет никаких специальных конструкторов. В результате он автоматически получает инициализатор по-умолчанию. Этот дефолтный инициализатор (когда он доступен) всегда выступает основным инициализатором для класса и может быть использован для создания нового объекта типа Vehicle со значением numberOfWheels, равным 0:
let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)
@{13.2\2\3}
Следующий пример демонстрирует подкласс Bicycle класса Vehicle:
class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}
Подкласс Bicycle определяет специальный основной конструктор init(). Этот основной инициализатор совпадает с основным конструктором суперкласса для Bicycle, так что версия этого конструктора у Bicycle помечена модификатором override.

Конструктор init() для Bicycle начинается с вызова super.init(), который вызывает дефолтный конструктор для надкласса класса Bicycle - Vehicle. Это гарантирует, что унаследованное свойство numberOfWheels инициализируется классом Vehicle прежде, чем класс Bicycle получает возможность изменить это свойство. После вызова super.init() изначальное значение numberOfWheels заменяется новым значением 2.
@{13.2\2\4}
Если Вы создадите экземпляр класса Bicycle, Вы сможете вызвать его унаследованное вычисляемое свойство description для того, чтобы увидеть, как изменилось его свойство numberOfWheels:
let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)
Подклассы могут изменять унаследованные переменные свойства в процессе инициализации, но не могут изменять константные свойства.
Автоматическое наследование инициализаторов
Как сказано выше, подклассы не наследуют инициализаторы своих надклассовая по-умолчанию. Однако инициализаторы надкласса могут быть автоматически унаследованы, если выполнены определённые условия. На практике это означает, что Вам не нужно писать перезаписи конструктор во многих общих сценариях, и что Ваши инициализаторы надкласса могут быть унаследованы с минимальными трудозатратами, когда это безопасно.

Приняв, что Вы предоставляете значения по-умолчанию для всех новых свойств, представленных в подклассе, применим два следующих правила:

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

Правило 2:
Если Ваш подкласс предоставляет реализации для всех основных инициализаторов надкласса - будь то с помощью наследованию согласно правилу 1 или же предоставив собственную реализацию в качестве части своего определение - тогда он унаследует автоматически все вспомогательные инициализаторы надкласса.

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

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

Рисунок демонстрирует цепочки инициализаторов для класса Food:
Рисунок 13.2.5
@{13.2\3\1}
Базовый класс в иерархии называется Food, который является простым классом для инкапсуляции названия еды. Класс Food вводит единственное свойство name типа String и предоставляет два конструктора для создания объектов типа Food:
class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}
@{13.2\3\2}
Классы не имеют дефолтных инициализаторов по-умолчанию, так что класс Food предоставляет основной инициализатор, который принимает один аргумент с названием name. Этот конструктор может быть использован для создания нового объекта типа Food со специфическим именем:
let namedMeat = Food(name: "Bacon")
// имя namedMeat равно "Bacon"
Конструктор init(name: String) класса Food представлен в качестве основного инициализатора, так как он убеждается, что все хранимые свойства нового объекта типа Food полностью проинициализированы. Класс Food не имеет надкласса, так что конструктору init(name: String) не нужно вызывать super.init() для завершения своей инициализации.
@{13.2\3\3}
Класс Food также предоставляет вспомогательный конструктор init() без аргументов. Конструктор init() предоставляет стандартное замещающее имя для любой новой еды, путём делегации в конструктор класса init(name: String) значение имени name величины [Unnamed]:
let mysteryMeat = Food()
// имя mysteryMeat равно "[Unnamed]"
@{13.2\3\4}
Второй класс в иерархии - это подкласс класса Food с названием RecipeIngredient. Класс RecipeIngredient моделирует ингредиент в поваренном рецепте. Он представляет свойство Int с названием quantity (в дополнение к свойству 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)
    }
}
Рисунок 13.2.6
Рисунок ниже демонстрирует цепочку инициализатора для класса RecipeIngredient:
Пример класса-ингредиента
Класс RecipeIngredient имеет единственный основной конструктор с названием init(name: String, quantity: Int), который может быть использован для задания всех свойств нового объекта типа RecipeIngredient. Этот конструктор начинает свою работу с присваивания переданного аргумента quantity одноимённому свойству, которое является единственным новым свойством, представленным в RecipeIngredient. После этого конструктор делегирует вверх конструктору init(name: String) класса Food. Этот процесс удовлетворяет проверки 1 из {Двухфазовой инициализации} выше.

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

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

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

В этом примере надкласс для RecipeIngredient - это Food, который имеет вспомогательный инициализатор init(). Этот инициализатор наследуется RecipeIngredient. Унаследованная версия init() функционирует тем же способом, что и его версия в Food, исключая то, что она делегирует RecipeIngredient-версии конструктора init(name: String) вместо версии Food.
@{13.3\3\5}
Все эти инициализаторы могут быть использованы для создания новых объектов типа RecipeIngredient:
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
Класс элемента списка покупок
Третий и последний класс в иерархии является подклассом RecipeIngredient с названием ShoppingListItem. ShoppingListItem-класс моделирует ингредиент рецепта в форме его появления в списке покупок.
Рисунок 13.2.7
Рисунок ниже демонстрирует общую инициализаторную цепочку для всех трёх классов:
@{13.3\3\6}
Всякая вещь в списке покупок начинается в состоянии "некупленности". Для представления этого факта 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, так как вещи в листе покупок (как это смоделировано здесь) всегда начинают свой жизненный путь некупленными.

Так как он предоставляет дефолтные значения для всех своих свойств, которые он представляет, и не определяет никаких собственных инициализаторов, то ShoppingListItem автоматически наследует все основные и вспомогательные инициализаторы своего надкласса.
@{13.3\3\7}
Вы можете использовать все эти три унаследованных инициализаторы для создания новых объектов типа 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]. После создания массива имя первого элемента ShoppingListItem массива заменяется c "[Unnamed]" на "Orange juice", и он маркируется в качестве приобретенного. Вывод описания каждого элемента в массива показывает, что их изначальные состояния были заданы, как ожидалось.