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

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

Xcode: 
7.3
Swift: 
2.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

👍 Отлично! Теперь вы можете использовать эти принципы на практике.

Знакомство с двухфазной инициализацией

До сих пор код в ваших инициализаторах устанавливал значения свойств и вызывал другие инициализаторы. Это первая фаза инициализации, но на самом деле существует вторая фаза инициализации в Swift.

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

Двухфазная инициализация в действии

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

Осуществите выполнение следующей структуры CombustionChamberStatus, чтобы увидеть как работает двухфазная инициализация в Swift. Убедитесь в том, что вы видите результаты в Xcode’s Debug area.

struct CombustionChamberStatus {
  var temperatureKelvin: Double
  var pressureKiloPascals: Double
 
  init(temperatureKelvin: Double, pressureKiloPascals: Double) {
    print("Phase 1 init")
    self.temperatureKelvin = temperatureKelvin
    self.pressureKiloPascals = pressureKiloPascals
    print("CombustionChamberStatus fully initialized")
    print("Phase 2 init")
  }
 
  init(temperatureCelsius: Double, pressureAtmospheric: Double) {
    print("Phase 1 delegating init")
    let temperatureKelvin = temperatureCelsius + 273.15
    let pressureKiloPascals = pressureAtmospheric * 101.325
    self.init(temperatureKelvin: temperatureKelvin, pressureKiloPascals: pressureKiloPascals)
    print("Phase 2 delegating init")
  }
}
 
CombustionChamberStatus(temperatureCelsius: 32, pressureAtmospheric: 0.96)

Вы должны увидеть следующий результат в Debug Area:

Phase 1 delegating init
Phase 1 init
CombustionChamberStatus fully initialized
Phase 2 init
Phase 2 delegating init

Как вы можете видеть, фаза 1 начинается с вызова делегируемого инициализатора init(temperatureCelsius:pressureAtmospheric:), во время которой вы не можете использовать self. Фаза 1 заканчивается сразу после того как self.pressureKiloPascals присваивается значение в неделегируемом инициализаторе. Каждый инициализатор играет определенную роль на каждом этапе.

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

Что делать, если что-то идет не так?

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

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

Использование Проваливающихся Инициализаторов

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

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

struct TankStatus {
  var currentVolume: Double
  var currentLiquidType: String?
 
  init(currentVolume: Double, currentLiquidType: String?) {
    self.currentVolume = currentVolume
    self.currentLiquidType = currentLiquidType
  }
}
 
let tankStatus = TankStatus(currentVolume: 0.0, currentLiquidType: nil)

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

Начните с изменения инициализатора TankStatus на failable initializer, присоеденив ? к init:

init?(currentVolume: Double, currentLiquidType: String?) {

Кликните мышью клавишу на Option на tankStatus и обратите внимание, что инициализатор теперь возвращает опциональный TankStatus.

Обновите создание экземпляра tankStatus, чтобы он соответствовал следующему:

if let tankStatus = TankStatus(currentVolume: 0.0, currentLiquidType: nil) {
  print("Nice, tank status created.") // Printed!
} else {
  print("Oh no, an initialization failure occurred.")
}

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

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

init?(currentVolume: Double, currentLiquidType: String?) {
  if currentVolume < 0 {
    return nil
  }
  if currentVolume > 0 && currentLiquidType == nil {
    return nil
  }
  self.currentVolume = currentVolume
  self.currentLiquidType = currentLiquidType
}

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

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

if let tankStatus = TankStatus(currentVolume: -10.0, currentLiquidType: nil) {

Обратите внимание на то, как плейграунд оповещает, «Oh no, an initialization failure occurred» ("О, нет, инициализация не удалась.") Поскольку инициализация не удалась, проваливающийся инициализатор возвращает nil и утверждение if let выполняет условие else.

Выбрасывание из инициализатора (генерирование ошибки)

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

У вас осталась последняя структура, которую нужно реализовать: представление каждого астронавта. Напишем следующий код:

struct Astronaut {
  let name: String
  let age: Int
 
  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
}

Менеджер говорит вам, что у свойства name: String не должно быть пустым, а его age: Int от 18 до 70 лет.

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

enum InvalidAstronautDataError: ErrorType {
  case EmptyName
  case InvalidAge
}

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

Затем замените инициализатор Astronaut следующей реализацией:

init(name: String, age: Int) throws {
  if name.isEmpty {
    throw InvalidAstronautDataError.EmptyName
  }
  if age < 18 || age > 70 {
    throw InvalidAstronautDataError.InvalidAge
  }
  self.name = name
  self.age = age
}

Обратите внимание, что инициализатор теперь отмечен как throws, чтобы вызывающие знали, что могут возникнуть ошибки.

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

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

let johnny = try? Astronaut(name: "Johnny Cosmoseed", age: 42)

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

Чтобы увидеть, как инициализатор выбрасывает ошибку, измените возраст johnny на 17:

let johnny = try? Astronaut(name: "Johnny Cosmoseed", age: 17) // nil

Когда вы вызываете генерирующий инициализатор, то вы пишете ключевое слово try - или try?, или try!, чтобы определить, может ли он сгенерировать ошибку. В этом случае вы используете try? и значение, которое возвращается в случае ошибки, равно nil. Обратите внимание на то, что значение johnny равно nil. Семнадцать - это, к сожалению, слишком рано для космического полета. Повезет в следующем году, Джонни!

Так что лучше Fail или Throw?

Инициализация, использующая генерирующий инициализатор вместе с try?, ужасно похожа на проваливающуюся инициализацию. Так какой инициализатор лучше использовать?

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

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

Почему тогда в Swift вообще есть проваливающиеся инициализаторы? А потому, что в первой версии Swift не было способа генерации ошибок, поэтому языку потребовался способ управления сбоями при инициализации.

Что дальше?

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

Урок подготовил: Акулов Иван

Источник урока: Источник