Подробно об инициализации. Часть 1/2

Туториалы

Подробно об инициализации. Часть 1/2

Некоторые вещи удивительны по своей природе: ракеты, полеты на Марс, инициализации в Swift. Этот туториал - просто блюдо 3-в-1 по удивительности. В нем вы узнаете о настоящей силе инициализации!

Инициализации в Swift - это то, что происходит, когда вы создаете новый экземпляр именованного типа:

let number = Float()

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

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

Перед тем как приступить к туториалу, вы уже должны быть знакомы с основами инициализации в Swift и свободно владеть такими понятиями, как опциональные типы, генерация (throwing) и обработка ошибок (error-handling), и объявление начальных значений для свойств хранения. Кроме того, убедитесь, что у вас установлен Xcode 7.3 или более поздняя версия.

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

Приступим

Давайте представим: это ваш первый день на новой работе в качестве инженера программного обеспечения в НАСА (Ого-го!). Вам была поставлена задача разработать модель данных, которая будет управлять последовательностью запуска первого пилотируемого полета на Марс, «Mars Unum». Конечно, первое, что вы сделаете, это убедите команду использовать Swift. Затем …

Откройте Xcode и создайте новый плейграунд под названием BlastOff. Можете выбрать любую платформу, так как код в этом туториале не зависит от платформы, а зависит только от фреймворка Foundation.

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

Рассмотрим дефолтный инициализатор

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

struct RocketConfiguration { }

Под закрывающей фигурной скобкой определения RocketConfiguration, инициализируйте постоянный экземпляр athena9Heavy:

let athena9Heavy = RocketConfiguration()

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

Добавьте следующие три свойства хранения внутри определения структуры:

let name: String = "Athena 9 Heavy" 
let numberOfFirstStageCores: Int = 3
let numberOfSecondStageCores: Int = 1

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

А что насчет опциональных типов? Добавьте свойство хранения numberOfStageReuseLandingLegs к определению структуры:

var numberOfStageReuseLandingLegs: Int?

По нашей легенде с НАСА, некоторые из ракет многоразового использования, а другие нет. Вот почему numberOfStageReuseLandingLegs - это опциональный Int. Дефолтный инициализатор продолжает нормально работать, потому что опциональные свойства хранения переменных инициализируются по умолчанию как nil. НО это не работает с константами.

Изменените numberOfStageReuseLandingLegs из переменной в константу:

let numberOfStageReuseLandingLegs: Int?

Плейграунд выдает ошибку компиляции:

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

let numberOfStageReuseLandingLegs: Int? = nil

Ура! Компилятор снова счастлив, и инициализация проходит успешно. С такой постановкой у numberOfStageReuseLandingLegs никогда не будет значения не nil. Вы не сможете изменить его после инициализации, так как оно объявлено как константа.

Рассмотрим почленный инициализатор (Memberwise Initializer)

У ракеты, как правило, есть ступени, давайте займемся этим после моделирования. Объявите новую структуру под названием RocketStageConfiguration в нижней части плейграунда:

struct RocketStageConfiguration {
  let propellantMass: Double
  let liquidOxygenMass: Double
  let nominalBurnTime: Int
}

На этот раз, у вас есть три свойства хранения: propellantMass, liquidOxygenMass и nominalBurnTime без начальных значений.

Создайте экземпляр RocketStageConfiguration для первой ступени ракеты:

let stageOneConfiguration = RocketStageConfiguration(propellantMass: 119.1, liquidOxygenMass: 276.0, nominalBurnTime: 180)

Ни одно из свойств хранения RocketStageConfiguration не имеет начального значения. Кроме того, нет инициализатора, реализованного для RocketStageConfiguration. Почему тогда не выходит ошибка компилятора? Структуры в Swift (и только структуры) автоматически генерируют memberwise initializer (почленный инициализатор). Это означает, что вы получаете готовый инициализатор для всех свойств хранения, у которых нет начальных значений. Это супер удобно, но тут есть несколько подводных камней.

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

Обновите RocketStageConfiguration для упорядочевания свойств хранения:

struct RocketStageConfiguration {
  let liquidOxygenMass: Double
  let nominalBurnTime: Int
  let propellantMass: Double
}

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

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

struct RocketStageConfiguration {
  let propellantMass: Double
  let liquidOxygenMass: Double
  let nominalBurnTime: Int
}

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

let nominalBurnTime: Int = 180

Теперь выходит другая ошибка компиляции:

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

Удалите дефолтное значение nominalBurnTime, чтобы устранить ошибку компилятора:

let nominalBurnTime: Int

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

init(propellantMass: Double, liquidOxygenMass: Double) {
  self.propellantMass = propellantMass
  self.liquidOxygenMass = liquidOxygenMass
  self.nominalBurnTime = 180
}

Обратите внимание, что ошибка компиляции теперь опять выходит на stageOneConfiguration:

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

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

Удалите аргумент nominalBurnTime из инициализации stageOneConfiguration:

let stageOneConfiguration = RocketStageConfiguration(propellantMass: 119.1, liquidOxygenMass: 276.0)

Все снова работает! :]

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

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

struct RocketStageConfiguration {
  let propellantMass: Double
  let liquidOxygenMass: Double
  let nominalBurnTime: Int
}
 
extension RocketStageConfiguration {
  init(propellantMass: Double, liquidOxygenMass: Double) {
    self.propellantMass = propellantMass
    self.liquidOxygenMass = liquidOxygenMass
    self.nominalBurnTime = 180
  }
}

Обратите внимание, как stageOneConfiguration продолжает успешно инициализировать с двумя параметрами. Теперь снова добавьте параметр nominalBurnTime к инициализации stageOneConfiguration:

let stageOneConfiguration = RocketStageConfiguration(propellantMass: 119.1, liquidOxygenMass: 276.0, nominalBurnTime: 180)

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

Реализация пользовательского инициализатора

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

struct Weather {
  let temperatureCelsius: Double
  let windSpeedKilometersPerHour: Double
}

У struct есть свойства хранения температуры в градусах Цельсия и скорости ветра в километрах в час.

Реализуйте пользовательский инициализатор для Weather, которая принимает температуру в градусах по Фаренгейту и скорость ветра в милях в час. Добавьте этот код ниже свойств хранения:

init(temperatureFahrenheit: Double, windSpeedMilesPerHour: Double) {
  self.temperatureCelsius = (temperatureFahrenheit - 32) / 1.8
  self.windSpeedKilometersPerHour = windSpeedMilesPerHour * 1.609344
}

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

Измените определение инициализатора на:

init(temperatureFahrenheit: Double = 72, windSpeedMilesPerHour: Double = 5) { ...

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

let currentWeather = Weather() 
currentWeather.temperatureCelsius 
currentWeather.windSpeedKilometersPerHour

Круто, не так ли? Дефолтный инициализатор использует дефолтные значения, предоставленные пользовательским инициализатором. Реализация пользовательского инициализатора переводит значения в метрическую систему эквивалентов и сохраняет значения. При проверке значения свойств хранения на боковой панели плейграунда, вы получите правильные значения в градусах Цельсия (22.2222) и километрах в час (8.047).

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

Затем измените currentWeather, чтобы использовать ваш пользовательский инициализатор с новыми значениями:

let currentWeather = Weather(temperatureFahrenheit: 87, windSpeedMilesPerHour: 2)

Как вы можете видеть, пользовательские значения работают точно так же в инициализаторе как и дефолтные значения. На боковой панели плейграунда теперь должны появиться 30.556 (градусов) и 3,219 (км/ч).

Вот так вы реализуете и вызываете пользовательский инициализатор. Ваша новая структура (Weather struct) готова внести свой вклад в миссию запуска человека на Марс. Отличная работа!

Избегаем дублирования с делегирующим инициализатором

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

struct GuidanceSensorStatus {
  var currentZAngularVelocityRadiansPerMinute: Double
  let initialZAngularVelocityRadiansPerMinute: Double
  var needsCorrection: Bool
 
  init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Bool) {
    let radiansPerMinute = zAngularVelocityDegreesPerMinute * 0.01745329251994
    self.currentZAngularVelocityRadiansPerMinute = radiansPerMinute
    self.initialZAngularVelocityRadiansPerMinute = radiansPerMinute
    self.needsCorrection = needsCorrection
  }
}

Эта структура содержит текущую и начальную угловую скорость ракеты относительно Z-оси (насколько она вращается). Структура также отслеживает, необходима ли ракете поправка, чтобы остаться на своей траектории.

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

Вы довольный пишите код, когда приходят инженеры и говорят, что новая версия ракеты дает вам Int для needsCorrection вместо Bool. Инженеры говорят, что положительный интеджер должен интерпретироваться как true, в то время как нулевое и отрицательное значение следует интерпретировать как false. Ваша команда не готова изменить остальную часть кода, так как это изменение является частью будущей функции. Итак, как можно угодить инженерам запуска и в то же время сохранить определение структуры нетронутым?

Без проблем! - добавьте следующий пользовательский инициализатор ниже первого инициализатора:

init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Int) {
  let radiansPerMinute = zAngularVelocityDegreesPerMinute * 0.01745329251994
  self.currentZAngularVelocityRadiansPerMinute = radiansPerMinute
  self.initialZAngularVelocityRadiansPerMinute = radiansPerMinute
  self.needsCorrection = (needsCorrection > 0)
}

Этот новый инициализатор принимает Int вместо Bool в качестве конечного параметра. Тем не менее свойство хранения needsCorrection еще Bool, то есть вы учли их требования.

После того, как вы написали этот код, что-то внутри подсказывает вам, что можно сделать еще лучше. Так много повторов остальной части кода инициализатора! И если будет ошибка в конверсии в радианы, то вам придется исправлять ее в нескольких местах — этого можно избежать. Здесь пригодится initializer delegation или делегирование инициализатора.

Замените инициализатор, который вы только что написали, следующим:

init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Int) {
  self.init(zAngularVelocityDegreesPerMinute: zAngularVelocityDegreesPerMinute,
   needsCorrection: (needsCorrection > 0))
}

Это инициализатор является delegating initializer (делегирующим инициализатором) и именно так как и звучит, он делегирует инициализацию на другой инициализатор. Делегировать - значит передать или делегировать свою работу, в нашем же случае просто вызвать любой другой инициализатор с помощью self.

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

Для проверки инициализатора, создайте экземпляр переменной с именем guidanceStatus:

let guidanceStatus = GuidanceSensorStatus(zAngularVelocityDegreesPerMinute: 2.2, needsCorrection: 0)
guidanceStatus.currentZAngularVelocityRadiansPerMinute // 0.038
guidanceStatus.needsCorrection // false

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

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

init(zAngularVelocityDegreesPerMinute: Double) {
  self.needsCorrection = false
  self.init(zAngularVelocityDegreesPerMinute: zAngularVelocityDegreesPerMinute,
    needsCorrection: self.needsCorrection)
}

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

Понимая это, удалите новый инициализатор и дайте аргументу needsCorrection основного инициализатора дефолтное значение false:

init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Bool = false) {

Обновите инициализацию guidanceStatus, удалив аргумент needsCorrection:

let guidanceStatus = GuidanceSensorStatus(zAngularVelocityDegreesPerMinute: 2.2) 
guidanceStatus.currentZAngularVelocityRadiansPerMinute // 0.038 
guidanceStatus.needsCorrection // false

 

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: