Инициализация
Инициализация - подготовительный процесс экземпляра класса, структуры или перечисления для дальнейшего использования. Этот процесс включает в себя установку начальных значений для каждого свойства хранения этого экземпляра и проведение любых настроек или инициализации, которые нужны до того, как экземпляр будет использоваться.
Вы реализуете эту инициализацию, определяя инициализаторы, которые схожи со специальными методами, которые вызываются для создания экземпляра определенного типа. В отличии от инициализаторов в Objective-C, инициализаторы в Swift не возвращают значения. Основная роль инициализаторов - убедиться в том, что новый экземпляр типа правильно инициализирован до того, как будет использован в первый раз.
Экземпляры классовых типов также могут реализовывать деинициализаторы, которые проводят любую чистку прямо перед тем, как экземпляр класса будет освобожден. Для более подробной информации читайте Деинициализация.
Установка начальных значений для свойств хранения
Классы и структуры должны устанавливать начальные значения у всех свойств хранения во время создания класса или структуры. Свойства хранения не могут быть оставлены в неопределённом состоянии.
Вы можете установить начальное значение свойства внутри инициализатора или присвоить ему значение по умолчанию, как часть определения свойства. Эти действия будут описаны подробнее в следующих секциях.
Заметка
Когда вы присваиваете значение по умолчанию свойству хранения или устанавливаете исходное значение в инициализаторе, то значение устанавливается напрямую, без вызова наблюдателей.
Инициализаторы
Инициализаторы вызываются для создания нового экземпляра конкретного типа. В самой простой своей форме инициализатор работает как метод экземпляра без параметров, написанный с помощью ключевого слова init:
init() {
// инициализация проводится тут
}
Пример ниже определяет новую структуру Fahrenheit для хранения температур, представленных в Фаренгейтах. Структура Fahrenheit имеет всего одно свойство, temperature типа Double:
struct Fahrenheit {
var temperature: Double
init() {
temperature = 32.0
}
}
var f = Fahrenheit()
print("Значение температуры по умолчанию \(f.temperature)° по Фаренгейту")
// Выведет "Значение температуры по умолчанию 32.0° по Фаренгейту"
Структура определяет один инициализатор, init, без параметров, который инициализирует хранимую температуру равную 32.0 (температура замерзания воды по Фаренгейту).
Дефолтные значения свойств
Вы можете установить исходное значение свойства в инициализаторе, как показано выше. Так же вы можете указать значение свойства по умолчанию, как часть определения свойства. Вы указываете значение свойства по умолчанию, написав исходное значение свойства, когда оно определено.
Заметка
Если свойство каждый раз берет одно и то же исходное значение, то лучше указать это значение, в качестве значения по умолчанию, чем каждый раз устанавливать его в инициализаторе. Конечный результат такой же, но значение по умолчанию связывает инициализацию свойства ближе к своему объявлению. Так делают, чтобы оставить инициализаторы в более чистой и краткой форме, и это позволяет вам вывести тип свойства из его значения по умолчанию. Значения по умолчанию так же дают вам больше преимуществ для использования инициализаторов по умолчанию или наследования инициализатора, что описано подробнее далее в этой главе.
Вы можете написать структуру Fahrenheit в более простой форме, указав значение по умолчанию для свойства temperature, в месте его объявления:
struct Fahrenheit {
var temperature = 32.0
}
Настройка инициализации
Вы можете настроить процесс инициализации входными параметрами и опциональными типами свойств или присваиванием значений для постоянных свойств во время инициализации, что будет описано далее.
Параметры инициализации
Вы можете показать параметры инициализации как часть определения инициализатора, для определения типов и имен значений, которые настраивают процесс инициализации. Параметры инициализации имеют те же возможности и синтаксис как и параметры функции или метода.
Следующий пример определяет структуру Celsius, которая хранит температуру в Цельсиях. Структура Celsius реализует два пользовательских инициализатора init(fromFahrenheit: ) и init(fromKelvin: ), которые инициализируют новый экземпляр структуры со значением другой температурной шкалы:
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0
Первый инициализатор имеет один параметр с внешним именем fromFahrenheit и с локальным именем fahrenheit. Второй инициализатор имеет один параметр с внешним именем fromKelvin и локальным именем kelvin. Оба инициализатора конвертируют их единственный аргумент в значение по Цельсию и сохраняют это значение в свойство temperatureInCelsius.
Локальные и внешние имена параметров
Как и в случае с параметрами функций или методов, параметры инициализации могут иметь локальные имена для использования внутри тела инициализатора и внешние для использования при вызове инициализатора.
Однако инициализаторы не имеют своего имени до круглых скобок, как это имеют методы или функции. Поэтому имена и типы параметров инициализатора играют важную роль в определении того, какой инициализатор и где может быть использован. Из-за этого Swift предоставляет автоматические внешние имена для каждого параметра, если вы, конечно, не укажете свое внешнее имя.
Следующий пример определяет структуру Color с тремя постоянными свойствами red, green, blue. Эти свойства имеют значения от 0.0 до 1.0, для индикации количества соответствующего цвета.
Color имеет инициализатор с тремя параметрами red, green, blue типа Double(компоненты цвета красного, зеленого, синего). Также Color имеет второй инициализатор с одним параметром white, который нужен для предоставления значения для всех трех компонентов цвета.
struct Color {
let red, green, blue: Double
init(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
init(white: Double) {
red = white
green = white
blue = white
}
}
Оба инициализатора могут быть использованы для создания нового экземпляра Color, передав значения в каждый параметр инициализатора:
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)
Обратите внимание, что невозможно вызвать инициализатор без использования внешних имен. Внешние имена обязательно должны быть использованы в инициализаторе, если они определены, если пропустить их, то выскочит ошибка компиляции:
let veryGreen = Color(0.0, 1.0, 0.0)
// этот код вызовет ошибку компиляции, так как здесь нет внешних имен
Параметры инициализатора без внешних имен
Если вы не хотите использовать внешние имена для параметров инициализации, напишите подчеркивание (_) вместо явного указания внешнего имени для этого параметра, чтобы переопределить поведение по умолчанию.
Вот расширенный вариант для Celsius, который мы рассматривали ранее, с дополнительным инициализатором, для создания нового экземпляра Celsius с типом значения температуры Double только уже по шкале Цельсия:
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
init(_ celsius: Double) {
temperatureInCelsius = celsius
}
}
let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius is 37.0
Инициализатор вызывает Celsius(37.0), что понятно и без внешнего имени параметра. Поэтому целесообразно написать init(_ celsius: Double), для того, чтобы предоставить безымянное значение типа Double.
Опциональные типы свойств
Если ваш пользовательский тип имеет свойство, которое логически имеет “отсутствие значения”, возможно потому, что его значение не может быть установлено во время инициализации или потому, что ему разрешается иметь “отсутствие значения” в какой-либо точке кода, то такое свойство нужно объявить с опциональным типом. Свойства опционального типа автоматически инициализируются со значением nil, указывая на то, что значение стремится иметь значение “пока что отсутствие значение” на этапе инициализации.
Следующий код определяет класс SurveyQuestion с опциональным типом String свойства response:
class SurveyQuestion {
var text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let cheeseQuestion = SurveyQuestion(text: "Нравится ли вам сыр?")
cheeseQuestion.ask()
// Выведет "Нравится ли вам сыр?"
cheeseQuestion.response = "Да, я люблю сыр"
Ответ на вопрос не может быть известен до того, пока он не задан, так что свойство response должно быть типа String? (опциональный String). Ему автоматически присваивается значение nil при инициализации SurveyQuestion, значащее, что “значения пока нет”.
Присваивание значений постоянным свойствам во время инициализации
Вы можете присвоить значения постоянных (когда свойство константа) свойств в любой точке вашего процесса инициализации. Как только для свойства константы присваивается значение, оно не может быть далее изменено.
Заметка
В экземплярах класса постоянное свойство может быть изменено только во время инициализации класса, в котором оно представлено. Оно не может быть изменено подклассом.
Вы можете пересмотреть пример SurveyQuestion и использовать вместо переменного свойства text постоянное свойство text, для индикации того, что это свойство не меняется после создания SurveyQuestion. Даже если свойство является постоянным, оно все еще может быть установлено в инициализаторе класса:
class SurveyQuestion {
let text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let beetsQuestion = SurveyQuestion(text: "Что насчет свеклы?")
beetsQuestion.ask()
// Выведет "Что насчет свеклы?"
beetsQuestion.response = "Я люблю свеклу, но не в сыром виде!"
Дефолтные инициализаторы
Swift предоставляет дефолтный инициализатор для любой структуры или базового класса, который имеет значение по умолчанию для всех его свойств и не имеет ни одного инициализатора. Дефолтный инициализатор просто создает новый экземпляр со всеми его свойствами с уже присвоенными значениями по умолчанию.
Этот пример определяет класс ShoppingListItem, который включает в себя имя, количество и состояние сделки на предмет из листа покупок:
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
Так как все свойства класса ShoppingListItem имеют значения по умолчанию и так как этот класс не имеет суперкласса, то ShoppingListItem автоматически получает реализацию дефолтного инициализатора, который создает новый экземпляр со всеми свойствами с уже присвоенными значениями по умолчанию. (Свойство name - свойство опционального типа String, значит значение по умолчанию равно nil). В примере выше используется дефолтный инициализатор для класса ShoppingListItem для создания нового экземпляра. Синтаксис дефолтного инициализатора в нашем случае выглядит как ShoppingListItem(), что присваивается переменной item.
Почленные инициализаторы структурных типов
Структурные типы автоматически получают почленный инициализатор, если они не определяют своего пользовательского инициализатора. Это верно даже при условии, что хранимые свойства не имеют значений по умолчанию.
Почленный инициализатор - сокращенный способ инициализировать свойства члена нового экземпляра структуры. Начальные значения для свойств нового экземпляра могут быть переданы в почленный инициализатор по имени.
Пример ниже определяет структуру Size с двумя свойствами width, height. Оба свойства выведены как Double из-за начального значения равного 0.0.
Структура Size автоматически получает init(width:height: ) почленный инициализатор, который вы можете использовать для инициализации Size экземпляра:
struct Size {
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
Делегирование инициализатора для типов значения
Инициализаторы могут вызывать другие инициализаторы для инициализации части экземпляра. Этот процесс называется как делегирование инициализатора. Он позволяет избегать дублирования кода в разных инициализаторах.
Правила того, как работает делегирование инициализатора и для каких форм делегирования это возможно, для типов значений и ссылочных типов разные. Типы значений (структуры и перечисления) не поддерживают наследование, так что их процесс делегирования инициализатора сравнительно прост, потому что они только могут делегировать другому инициализатору то, что предоставляют сами. Классы, однако, могут наследовать от других классов, как это описано в Наследование. Это значит, что у классов есть дополнительная ответственность за проверку наличия корректных значений у каждого унаследованного свойства хранения класса. Обязанности описаны в главе Наследование и инициализация класса.
Для типов значений вы используете self.init для ссылки на остальные инициализаторы одного и того же типа значения, когда вы пишете свои инициализаторы. Вы можете вызывать self.init из инициализатора.
Обратите внимание, что если вы определите пользовательский инициализатор для типов значений, то вы больше не будете иметь доступа к дефолтному инициализатору (или почленному инициализатору, если это структура) для этого типа. Такое ограничение предотвращает ситуацию, в которой настройка важного дополнения в более сложном инициализаторе может быть пропущена при случайном использовании автоматического инициализатора.
Заметка
Если вы хотите, чтобы ваш пользовательский тип значения имел возможность быть инициализированным дефолтным инициализатором или почленным инициализатором, или вашим пользовательским инициализатором, то вам нужно написать свой пользовательский инициализатор в расширении вашего типа, чем как часть реализации типа значения. Для более подробной информации смотрите Расширения.
Следующий пример определяет пользовательскую структуру Rect для отображения геометрического прямоугольника. Примеру нужно добавить две вспомогательные структуры Size, Point, каждая из которых предоставляет значения по умолчанию 0.0 для своих свойств:
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
Вы можете инициализировать структуру 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)
}
}
Первый инициализатор Rect - init() функционально тот же самый, что и дефолтный инициализатор, который бы получила структура, если бы не имела пользовательских инициализаторов. Инициализатор имеет пустое тело, отображенное парой пустых фигурных скобок {}, и не проводит никакой инициализации. Вызывая такой инициализатор, мы возвращаем экземпляр Rect, который имеет инициализированные свойства origin, size значениями Point(x: 0.0, y: 0.0) и Size(width: 0.0, height: 0.0), которые известны из определения свойств:
let basicRect = Rect()
//исходная точка Rect (0.0, 0.0) и его размер (0.0, 0.0)
Второй инициализатор Rect - init(origin:size: ) функционально то же самое что и почленный инициализатор, который могла бы иметь структура, если бы не имела пользовательских инициализаторов. Этот инициализатор просто присваивает значения аргументов origin, size соответствующим свойствам:
let originRect = Rect(origin: Point(x: 2.0, y: 2.0), size: Size(width: 5.0, height: 5.0))
//исходная точка Rect (2.0, 2.0) и его размер (5.0, 5.0)
Третий инициализатор Rect - 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: ) воспользоваться преимуществом того, что существует другой инициализатор с абсолютно такой же функциональностью.
Заметка
Для альтернативного способа записи этого примера без инициализаторов init(), init(origin:size: ) смотрите главу Расширения.
Наследование и инициализация класса
Всем свойствам класса, включая и те, что унаследованы у суперкласса должны быть присвоены начальные значения, во время их инициализации.
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) колес(о)"
}
}
Класс Vehicle предоставляет значение по умолчанию для его единственного свойства, и не имеет никаких собственных пользовательских инициализаторов. И в результате он автоматически получает дефолтный инициализатор, как описано в главе Дефолтные инициализаторы. Дефолтный инициализатор (когда доступен) всегда является назначенным инициализатором для класса и может быть использован для создания нового экземпляра класса Vehicle с numberOfWheels равным 0:
let vehicle = Vehicle()
print("Транспортное средство \(vehicle.description)")
//Транспортное средство 0 колес(о)
Следующий пример определяет подкласс Bicycle суперкласса Vehicle:
class Bicycle: Vehicle {
override init() {
super.init()
numberOfWheels = 2
}
}
Подкласс Bicycle определяет пользовательский назначенный инициализатор init(). Назначенный инициализатор совпадает с назначенным инициализатором из суперкласса Vehicle и, таким образом, версия этого инициализатора класса 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. Потом мы выводим на экран описание каждого элемента массива, где мы можем видеть, что все начальные значения установлены так как мы и ожидали.
Проваливающиеся инициализаторы
Иногда бывает нужно определить класс, структуру или перечисление, инициализация которого может не сработать, провалиться. Такое неисполнение может быть вызвано некорректными значениями параметров или отсутствием требуемого внешнего источника данных или еще какое-нибудь обстоятельство, которое может не позволить завершить инициализацию успешно.
Для того чтобы справиться с условиями инициализации, которые могут провалиться, определите один или несколько проваливающихся инициализаторов как часть определения класса, структуры или перечисления. Вы можете написать проваливающийся инициализатор поместив вопросительный знак после ключевого слова init (init?).
Заметка
Вы не можете определить проваливающийся инициализатор и обычные инициализаторы с одними и теми же именами и типами параметров.
Проваливающийся инициализатор создает опциональное значение типа, который он инициализирует. Вы пишете return nil внутри проваливающегося инициализатора для индикации точки, где инициализация может провалиться.
Заметка
Строго говоря, инициализаторы не возвращают значений. Их роль заключается в том, что они проверяют, что self полностью и корректно инициализирован, до того, как инициализация закончится. Несмотря на то, что вы пишете return nil для указания неудачи инициализации, вы не пишете слово return в случае, если инициализация прошла успешно.
Например, проваливающиеся инициализаторы реализуются для преобразования числового типа. Для гарантии того, что преобразование между числовыми типами имеет смысл, используйте инициализатор init(exactly: ). Если преобразование невозможно, то данный инициализатор "провалится", то есть вернет nil.
let wholeNumber: Double = 12345.0
let pi = 3.14159
if let valueMaintained = Int(exactly: wholeNumber) {
print("\(wholeNumber) преобразование в Int поддерживает значение \(valueMaintained)")
}
// Выведет "12345.0 преобразование в Int поддерживает значение 12345"
let valueChanged = Int(exactly: pi)
// valueChanged is of type Int?, not Int
if valueChanged == nil {
print("\(pi) преобразование в Int невозможно")
}
// Выведет "3.14159 преобразование в Int невозможно"
Пример ниже определяет структуру Animal, с константным свойством типа String с именем species. Структура Animal так же определяет проваливающийся инициализатор с одним параметром species. Этот инициализатор проверяет равняется ли переданное значение из species в инициализатор пустой строке. Если строка действительно пустая, то возвращается nil, и срабатывает проваливающийся инициализатор. В противном случае значение свойства species установлено и инициализация проходит успешно.
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
Вы можете использовать этот проваливающий инициализатор для попытки инициализировать новый экземпляр структуры Animal и проверить успешно ли прошла инициализация:
let someCreature = Animal(species: "Жираф")
// someCreature имеет тип Animal?, но не Animal
if let giraffe = someCreature {
print("Мы инициализировали животное типа \(giraffe.species) ")
}
// Выведет "Мы инициализировали животное типа Жираф "
Если вы передаете пустую строку в параметр species проваливающегося инициализатора, то инициализатор вызывает сбой инициализации:
let anonymousCreature = Animal(species: "")
// anonymousCreature имеет тип Animal?, но не Animal
if anonymousCreature == nil {
print("Неизвестное животное не может быть инициализировано")
}
// Выведет "Неизвестное животное не может быть инициализировано"
Заметка
Проверяя значение пустой строки (к примеру "", а не "Жираф") это не тоже самое, что проверять на nil, для индикации отсутствия значения опционального String. В примере выше, пустая строка ("") корректна и является обычной String, а не String?. Однако это не допустимо в нашем случае, чтобы животное имело пустое значение, например, свойства species. Для того чтобы смоделировать такое ограничение, мы используем проваливающийся инициализатор, который выдает сбой, если находит пустую строку.
Проваливающиеся инициализаторы для перечислений
Вы можете использовать проваливающийся инициализатор для выбора подходящего члена перечисления основываясь на одном или более параметров. Инициализатор может провалиться, если предоставленные параметры не будут соответствовать подходящему члену перечисления.
Пример ниже определяет перечисление TemperatureUnit с тремя возможными вариантами (kelvin, celsius и fahrenheit). Проваливающийся инициализатор используется для того, чтобы найти подходящий член перечисления для значения типа Character, которое представляет символ температуры:
enum TemperatureUnit {
case kelvin, celsius, fahrenheit
init?(symbol: Character) {
switch symbol {
case "K":
self = .kelvin
case "C":
self = .celsius
case "F":
self = .fahrenheit
default:
return nil
}
}
}
Вы можете использовать этот проваливающийся инициализатор для выбора соответствующего члена из трех возможных состояний и вызвать провал инициализации, если параметр не соответствует этим состояниям:
let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
print("Эта единица измерения температуры определена, а значит наша инициализация прошла успешно!")
}
// Выведет "Эта единица измерения температуры определена, а значит наша инициализация прошла успешно!"
let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
print("Единица измерения температуры не определена, таким образом мы зафейлили инициализацию")
}
// Выведет "Единица измерения температуры не определена, таким образом мы зафейлили инициализацию"
Проваливающиеся инициализаторы для перечислений с начальными значениями
Перечисления с начальными значениями по умолчанию получают проваливающийся инициализатор init?(rawValue: ), который принимает параметр rawValue подходящего типа и выбирает соответствующий член перечисления, если он находит подходящий, или срабатывает сбой инициализации, если существующее значение не находит совпадения среди членов перечисления.
Вы можете переписать пример TemperatureUnit из примера выше для использования начальных значений типа Character и использовать инициализатор init?(rawValue: ):
enum TemperatureUnit: Character {
case kelvin = "K", celsius = "C", fahrenheit = "F"
}
let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
print("Эта единица измерения температура определена, а значит наша инициализация прошла успешно!")
}
// Выведет "Эта единица измерения температура определена, а значит наша инициализация прошла успешно!"
let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
print("Единица измерения температуры не определена, таким образом мы зафейлили инициализацию.")
}
// Выведет "Единица измерения температуры не определена, таким образом мы зафейлили инициализацию."
Распространение проваливающегося инициализатора
Проваливающийся инициализатор класса, структуры, перечисления может быть делегирован к другому проваливающемуся инициализатору из того же класса, структуры, перечисления. Аналогично проваливающийся инициализатор подкласса может быть делегирован наверх в проваливающийся инициализатор суперкласса.
В любом случае, если вы делегируете другому инициализатору, который проваливает инициализацию то и весь процесс инициализации проваливается немедленно за ним, и далее никакой код инициализации уже не исполняется.
Заметка
Проваливающийся инициализатор может также делегировать к непроваливающемуся инициализатору. Используя такой подход, вам следует добавить потенциальное состояние провала в существующий процесс инициализации, который в противном случае не провалится.
Пример ниже определяет подкласс CartItem класса Product. CartItem создает модель элемента в корзине онлайн заказа. CartItem представляет свойство хранения quantity и проверяет, чтобы это свойство всегда имело значение не менее 1:
class Product {
let name: String
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
class CartItem: Product {
let quantity: Int
init?(name: String, quantity: Int) {
if quantity < 1 { return nil }
self.quantity = quantity
super.init(name: name)
}
}
Проваливающийся инициализатор для CartItem начинается с того, что получает значение quantity 1 или более. Если значение quantity некорректное, то вся инициализация проваливается и код дальше не исполняется. Так же проваливающийся инициализатор Product проверяет значение свойства name, и если оно равно пустой строке, то инициализация немедленно прекращается.
Если вы создаете экземпляр CartItem с name не равной пустой строке и quantity равному 1 или более, то инициализация проходит успешно:
if let twoSocks = CartItem(name: "sock", quantity: 2) {
print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Выведет "Item: sock, quantity: 2"
Если вы попытаетесь создать экземпляр CartItem с quantity со значением 0, то инициализация провалится:
if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
print("Невозможно инициализировать ноль футболок")
}
// Выведет "Невозможно инициализировать ноль футболок"
Аналогично, если вы попытаетесь создать экземпляр CartItem с name равным пустой строке, то инициализатор суперкласса Product вызовет неудачу инициализации:
if let oneUnnamed = CartItem(name: "", quantity: 1) {
print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
print("Невозможно инициализировать товар без имени")
}
// Выведет "Невозможно инициализировать товар без имени"
Переопределение проваливающегося инициализатора
Вы можете переопределить проваливающийся инициализатор суперкласса в подклассе, так же как любой другой инициализатор. Или вы можете переопределить проваливающий инициализатор суперкласса непроваливающимся инициализатором подкласса. Это позволяет вам определить подкласс, для которого инициализация не может провалиться, даже когда инициализация суперкласса позволяет это сделать.
Запомните, что если вы переопределяете проваливающийся инициализатор суперкласса не проваливающимся инициализатором подкласса, то единственным способом делегировать в инициализатор суперкласса - принудительное извлечение результата из проваливающего инициализатора суперкласса.
Заметка
Вы можете переопределить проваливающийся инициализатор непроваливающимся инициализатором, но не наоборот.
Пример ниже определяет класс Document. Этот класс моделирует документ, который может быть инициализирован свойством name, которое может иметь значение отличное от пустого или nil, но никак не пустую строку:
class Document {
var name: String?
//этот инициализатор создает документ со значением nil свойства name
init(){}
//этот инициализатор создает документ с не пустым свойством name
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
Следующий пример определяет подкласс AutomaticallyNamedDocument класса Document. AutomaticallyNamedDocument является подклассом, который переопределяет оба назначенных инициализатора, представленных в Document. Это переопределение гарантирует, что экземпляр AutomaticallyNamedDocument будет иметь исходное значение "[Untitled]" свойства name, если экземпляр создан без имени или если пустая строка передана в инициализатор init(name: ):
class AutomaticallyNamedDocument: Document {
override init() {
super.init()
self.name = "[Untitled]"
}
override init(name: String) {
super.init()
if name.isEmpty {
self.name = "[Untitled]"
} else {
self.name = name
}
}
}
AutomaticallyNamedDocument переопределяет проваливающийся инициализатор init?(name: ) суперкласса непроваливающимся инициализатором init(name: ). Из-за того, что AutomaticallyNamedDocument справляется с пустой строкой иначе, чем его суперкласс, его инициализатор не обязательно должен провалиться, таким образом он предоставляет непроваливающуюся версию инициализатора вместо проваливающейся.
Вы можете использовать принудительное извлечение внутри инициализатора для вызова проваливающегося инициализатора из суперкласса, в качестве части реализации непроваливающегося инициализатора подкласса. Например, подкласс класса UntitledDocument всегда имеет имя "[Untitled]", и он использует проваливающийся init(name: ) из суперкласса во время инициализации.
class UntitledDocument: Document {
override init() {
super.init(name: "[Untitled]")!
}
}
В этом случае инициализатор суперкласса init(name: ) каждый раз будет вызывать ошибку исполнения, если в него будет передана пустая строка. Однако, так как этот инициализатор теперь имеет строковую константу, то этот инициализатор больше не провалится, то есть ошибки исполнения больше не будет.
Проваливающийся инициализатор init!
Обычно вы определяете проваливающийся инициализатор, который создает опциональный экземпляр соответствующего типа путем размещения знака вопроса после ключевого слова init (init?). Альтернативно, вы можете определить проваливающийся инициализатор, который создает экземпляр неявно извлекаемого опционала соответствующего типа. Сделать это можно, если вместо вопросительного знака поставить восклицательный знак после ключевого слова init (init!).
Вы можете делегировать от init? в init! и наоборот, а так же вы можете переопределить init? с помощью init! и наоборот. Вы так же можете делегировать от init в init!, хотя, делая таким образом, мы заставим сработать утверждение, если init! провалит инициализацию.
Требуемые инициализаторы
Напишите required перед определением инициализатора класса, если вы хотите, чтобы каждый подкласс этого класса был обязан реализовывать этот инициализатор:
class SomeClass {
required init() {
//пишем тут реализацию инициализатора
}
}
Вы также должны писать модификатор required перед каждой реализацией требуемого инициализатора класса для индикации того, что последующий подкласс так же должен унаследовать этот инициализатор по цепочке. Вы не пишете override, когда переопределяете требуемый инициализатор:
class SomeSubclass: SomeClass {
required init() {
//пишем тут реализацию инициализатора подкласса
}
}
Заметка
Вы не должны обеспечивать явную реализацию требуемого инициализатора, если вы можете удовлетворить требование унаследованным инициализатором.
Начальное значение свойства в виде функции или замыкания
Если начальное значение свойства требует какой-то настройки или структуризации, то вы можете использовать замыкание или глобальную функцию, которая будет предоставлять значение для этого свойства. Как только создается новый экземпляр, вызывается функция или замыкание, которая возвращает значение, которое присваивается в качестве начального значения свойства.
Эти виды замыканий или функций обычно создают временное значение того же типа, что и свойство, используя эту величину для отображения желаемого начального состояния, затем возвращают ее в качестве начального значения свойства.
Ниже приведена схема того, как замыкание может предоставлять начальное значение свойству:
class SomeClass {
let someProperty: SomeType = {
// создаем начальное значения для SomeProperty внутри этого замыкания
// someValue должен быть того же типа, что и SomeType
return someValue
}()
}
Обратите внимание, что после закрывающей фигурной скобки замыкания идут пустая пара круглых скобок. Это означает, что нужно исполнить это замыкание немедленно. Если вы пропустите эти скобки, то вы присваиваете само значение замыкания свойству, а не возвращаете значения замыкания.
Заметка
Если вы используете замыкание для инициализации свойства, помните, что остальная часть экземпляра еще не инициализирована, на тот момент когда исполняется замыкание. Это значит, что вы не можете получить доступ к значениям других свойств из вашего замыкания, даже если эти свойства имеют начальное значение. Вы так же не можете использовать неявное свойство self и не можете вызвать какой-либо метод вашего экземпляра.
Пример ниже определяет структуру Chessboard, которая моделирует доску для игры “Шашки”:
В игру шашки играют на доске 8х8, с чередующимися черными и белыми клетками. Для отображения игральной доски для этой игры, структура Chessboard имеет единственное свойство boardColors, которое является массивом из 64 значений типа Bool. Значение true отображает черные клетки, false - белые. Первое значение массива отображает левый верхний угол доски, значение последнего элемента отображает правый нижний угол доски.
Массив boardColors инициализирован с помощью замыкания для установки значений цветов:
struct Chessboard {
let boardColors: [Bool] = {
var temporaryBoard = [Bool]()
var isBlack = false
for i in 1...8 {
for j in 1...8 {
temporaryBoard.append(isBlack)
isBlack = !isBlack
}
isBlack = !isBlack
}
return temporaryBoard
}()
func squareIsBlackAt(row: Int, column: Int) -> Bool {
return boardColors[(row * 8) + column]
}
}
Когда бы вы ни создали новый экземпляр Chessboard, замыкание исполняется, и начальное значение boardColors высчитывается и возвращается. Замыкание в примере выше высчитывает временный массив temporaryBoard и возвращает его в качестве возвращаемого значения замыканием, как только он весь заполняется. Возвращенный массив сохраняется в boardColors и может быть запрошен через метод squareIsBlackAt:
let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Выведет "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Выведет "false"