Основы Swift / 13.3. Продвинутая инициализация


Видео


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

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

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

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

Строго говоря, инициализатор не возвращают значения. Вместо этого их цель состоит в том, чтобы убедиться, что self полностью и корректно инициализирован к моменту окончания инициализации. Несмотря на это Вы можете написать return nil, чтобы вызвать провал инициализации, однако Вы не используете ключевое слово return для указания на удачную инициализацию.
@{13.3\1}
Для примера, проваливающиеся инициализаторы реализованы для преобразования числовых типов. Чтобы убедиться, что преобразование между числовыми типами полностью преобразует значение, используется конструктор init(exactly:). Если преобразование типов не может полностью преобразовать значение, то инициализация проваливается.
let wholeNumber: Double = 12345.0
let pi = 3.14159

if let valueMaintained = Int(exactly: wholeNumber) {
    print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)")
}
// Выведет "12345.0 conversion to Int maintains value of 12345"

let valueChanged = Int(exactly: pi)
// valueChanged имеет тип Int?, а не Int

if valueChanged == nil {
    print("\(pi) conversion to Int does not maintain value")
}
// Выведет "3.14159 conversion to Int does not maintain value"
@{13.3\2\1}
Пример ниже определяет структуру с названием Animal с константным свойством species типа String. Структура Animal также определяет проваливающийся инициализатор с единственным параметром с названием species. Инициализатор проверяет, если значение species передаётся в инициализатор в качестве пустой строки. Если найдена пустая строка, то сработает провал инициализации. В противном случае будет задано значения свойства species, а инициализация удастся:
struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}
@{13.3\2\2}
Вы можете использовать этот проваливающийся инициализатор для попытки инициализировать новый объект типа Animal и проверить, удачна ли прошла инициализация:
let someCreature = Animal(species: "Giraffe")
// someCreature is of type Animal?, not Animal

if let giraffe = someCreature {
    print("An animal was initialized with a species of \(giraffe.species)")
}
// Выведет "An animal was initialized with a species of Giraffe"
@{13.3\2\3}
Если Вы передадите пустое строковое значение в проваливающийся параметр инициализатора species, то инициализатор выдаст ошибку инициализации:
let anonymousCreature = Animal(species: "")
// anonymousCreature имеет тип Animal?, а не Animal

if anonymousCreature == nil {
    print("The anonymous creature could not be initialized")
}
// Выведет "The anonymous creature could not be initialized"
Проверка на пустое строковое значение (как например "" вместо "Giraffe") - это не то же самое, что проверка на наличие nil для индикаии на отсутствие значения у объекта типа String-опционал. В примере выше пустая строка ("") - это корректная неопциональная строка. Однако недопустимо для животного иметь пустую строку в качестве значения species. Для моделирования этого ограничения проваливающийся конструктор вызывает ошибку инициализации, если получена пустая строка
Проваливающиеся инициализаторы для перечислений
Вы можете использовать проваливающийся инициализатор для выбора подходящего кейса перечисления на основании одного или нескольких параметров. Если предоставленные параметры не совпадают с подходящим кейсом перечисления, то инициализатор может провалиться.
@{13.3\3\1}
Пример ниже определяет перечисление с названием TemperatureUnit c тремя возможными состояниями (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
        }
    }
}
@{13.3\3\2}
Вы можете использовать этот проваливающийся инициализатор для выбора подходящего кейса перечисления для трёх возможных состояний и для вызова провала инициализации, если параметр не совпадает с одним из этих состояний:
let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// Выведет "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
    print("This is not a defined temperature unit, so initialization failed.")
}
// Выведет "This is not a defined temperature unit, so initialization failed."
Проваливающиеся инициализаторы для перечислений с чистыми значениями
Перечисления с чистыми значениями автоматически получают проваливающийся инициализатор init?(rawValue:), который принимает параметр с названием rawValue подходящего типа для чистого значения и выбирает подходящий кейс перечисления, если он найден, или вызывает провал инициализации, если совпадающих значений не существует.
@{13.3\4}
Вы можете переписать пример TemperatureUnit выше для использования чистых значений типа Character и для получения преимущества от инициализатора init?(rawValue:):
enum TemperatureUnit: Character {
    case kelvin = "K", celsius = "C", fahrenheit = "F"
}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// Выведет "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
    print("This is not a defined temperature unit, so initialization failed.")
}
// Выведет "This is not a defined temperature unit, so initialization failed."
Всплытие провала инициализации
Проваливающийся инициализатор класса, структуры или перечисления может делегировать управление другому проваливающемуся инициализатору того же класса, структуры или перечисления.

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

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

Проваливающийся инициализатор также может делегировать непроваливающемуся инициализатору. Используйте этот подход, если Вам нужно добавить потенциальное состояние провала для существующего процесса инициализации, который в противном случае не может провалиться.
@{13.3\5\1}
Пример ниже определяет подкласс класса Product с названием CartItem. Класс 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, а процесс инициализации немедленно провяливается, если name - это пустая строка.
@{13.3\5\2}
Если Вы создадите объект CartItem с негустым именем и количеством 1 или более, то инициализация будет успешной:
if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Выведет "Item: sock, quantity: 2"
@{13.3\5\3}
Если Вы создадите объект типа CartItem со значением свойства quantity, равным 0, то инициализатор CartItem вызовет провал:
if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
    print("Unable to initialize zero shirts")
}
// Выведет "Unable to initialize zero shirts"
@{13.3\5\4}
Аналогично, если Вы попытаетесь создать объект типа CartItem c пустым значением name, то инициализатор надкласса Product вызовет провал инициализации:
if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
    print("Unable to initialize one unnamed product")
}
// Выведет "Unable to initialize one unnamed product"
Перезапись проваливающегося инициализатора
Вы можете перезаписать проваливающийся инициализатор надкласса в его подклассе, как и любой другой инициализатор. С другой стороны, Вы можете перезаписать провальный инициализатор надкласса с помощью непроваливающегося инициализатора подкласса. Это позволяет Вам определить подкласс, для которого инициализация не может провалиться, даже если инициализации надкласса это разрешено.

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

Вы можете перезаписать проваливающийся инициализатор непроваливающимся, но не наоборот.
@{13.3\6\1}
Пример ниже определяет класс с названием Document. Это класс моделирует документ, который инициализируется с помощью свойства name, который может быть непустой строкой или nil, но пустой строкой быть не может:
class Document {
    var name: String?
    // this initializer creates a document with a nil name value
    init() {}
    // this initializer creates a document with a nonempty name value
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}
@{13.3\6\2}
Следующий пример определяет подкласс класса Document c названием AutomaticallyNamedDocument. Подкласс AutomaticallyNamedDocument перезаписывает оба основных конструктора, определённых в Document. Эти перезаписи гарантируют, что AutomaticallyNamedDocument получит изначальное значение name, равное "[Untitled]", если объект будет задан без имени или если в конструктор 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 взаимодействует с пустой строкой другим способ нежели его надкласс, то его инициализатору не должен проваливаться, а значит он предоставляет непроваливающуюся версию этого конструктора.
@{13.3\6\3}
Вы можете использовать принудительную распаковку в инициализаторе для вызова проваливающегося инициализатора из его суперкласса как часть реализации непроваливающегося инициализатора подкласса. Например, 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 перед определением инициализатора класса для указания на то, что каждый подкласс этого класса должен реализовать этот инициализатор:
@{13.3\7\1}
class SomeClass {
    required init() {
        // initializer implementation goes here
    }
}
Модификатор required
Вы должны также записывать модификатор required перед каждой реализацией этого инициализатора в подклассах, чтобы обозначить то, что инициализатор должен быть реализован во всех подклассах в цепочке. Вам не нужно писать модификатор override при переписывании требуемого основного инициализатора:
@{13.3\7\2}
class SomeSubclass: SomeClass {
    required init() {
        // subclass implementation of the required initializer goes here
    }
}
Удовлетворения требования на требуемый инициализатор
Вам не нужно предоставлять явную реализацию требуемого инициализатора, если Вы можете выполнить требование с помощью наследуемого инициализатора.
Установка значения по-умолчанию с помощью замыкания или функции
Если хранимое значение свойства по-умолчанию требует некоторой кастомизации или установки, то Вы можете использовать замыкание или глобальную функцию для предоставления кастомизированного значения для этого свойства по-умолчанию. Когда создаётся новый экземпляр типа, чьи свойства должны быть инициализированы, то замыкание или функция вызываются, а их возвращаемое значение присваивается изначальному значению свойства.

Эти типы замыканий или функций обычно создают временное значение того же типа что и свойство, говоря этим, что это значением отражает соответствующее начально состояние, а затем возвращают это временное значение для использования в качестве значения свойства по-умолчанию.
@{13.3\8}
class SomeClass {
    let someProperty: SomeType = {
        // create a default value for someProperty inside this closure
        // someValue must be of the same type as SomeType
        return someValue
    }()
}
Заметьте, что закрывающая фигурная скобка замыкания сопровождается пустой парой круглых скобок. Это говорит Swift исполнить замыкание немедленно. Если Вы опустите эти круглые скобки, то тем самым Вы попытаетесь присвоить замыкание самому свойству, а не его возвращаемое значение.

Если Вы используете замыкание для инициализации свойства, помните, что большая часть объекта ещё не инициализирована в момент выполнения замыкания. Это означает, что Вы не сможете получить доступ к любым другим свойствам из этого замыкания, даже если они имеют значения по-умолчанию. Вы так же не можете использовать неявно свойство self или вызывать любые методы объекта.
Пример шахматной доски
Пример ниже определяет структуру с названием Chessboard, которая моделирует доску для игры в шахматы. Шахматы играются на доске 8 x 8 с чередующимися чёрными и белыми квадратами.
Рисунок 13.3.1
@{13.3\9\1}
Чтобы представить игровую доску, структура 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]
    }
}
@{13.3\9\2}
Как только создаётся новый объект типа Chessboard, то выполняется замыкание и изначальное значение boardColors вычисляется и возвращается. Замыкание в примере выше вычисляет и задаёт подходящий цвет для каждого квадрата на игровой доске во временном массиве с названием temporaryBoard, а затем возвращает этот временный массив в качестве возвращаемого значение замыкания, когда его выполнение заканчивается. Этот массив сохраняется в boardColors и может быть запрошен с помощью функции-инструмента squareIsBlackAt(row:column:):
let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Выведет "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Выведет "false"