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

Проваливающиеся инициализаторы

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

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

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

Заметка

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

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

Заметка

Строго говоря, инициализаторы не возвращают значений. Их роль заключается в том, что они проверяют, что self полностью и корректно инициализирован, до того, как инициализация закончится. Несмотря на то, что вы пишите return nil для указания неудачи инициализации, вы не пишите слово return в случае, если инициализация прошла успешно.

Например, проваливающиеся инициализаторы реализуются для преобразования числового типа. Для гарантии того, что преобразование между числовыми типами имеет смысл, используйте инициализатор  init(exaclty:). Если преобразование невозможно, то данный инициализатор "провалится", то есть вернет nil.

let wholeNumber: Double = 12345.0
let pi = 3.14159
 
if let valueMaintained = Int(exactly: wholeNumber) {
    print("\(wholeNumber) преобразование в Int поддерживает значение \(valueMaintained)")
}
// Prints "12345.0 conversion to Int maintains value of 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: "")
// someCreature имеет тип 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 создает модель элемент в корзине онлайн заказа. CarItem представляет свойство хранения 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! провалит инициализацию.

 

Swift: 
4.0