Книга: Swift. Основы разработки приложений под iOS, iPadOS и macOS. 5-е изд. дополненное и переработанное
Назад: 28. Псевдонимы Any и AnyObject
Дальше: 30. Удаление экземпляров и ARC

29. Инициализаторы и деинициализаторы

Инициализатор — это специальный метод, выполняющий подготовительные действия при создании экземпляра объектного типа данных. Инициализатор срабатывает при создании экземпляра, а при его удалении вызывается деинициализатор.

29.1. Инициализаторы

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

Назначенные инициализаторы

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

• классы имеют пустой встроенный инициализатор init(){};

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

ПРИМЕЧАНИЕ Пустой инициализатор срабатывает без ошибок только в том случае, если у класса отсутствуют свойства или у каждого свойства указано значение по умолчанию.

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

Инициализаторы класса и структуры, производящие установку значений свойств, называются назначенными (designated). Вы можете разработать произвольное количество назначенных инициализаторов с отличающимся набором параметров в пределах одного объектного типа. При этом должен существовать хотя бы один назначенный инициализатор, производящий установку значений всех свойств (если они существуют), и один из назначенных инициализаторов должен обязательно вызываться при создании экземпляра. Назначенный инициализатор не может вызывать другой назначенный инициализатор, то есть использование конструкции self.init() запрещено.

ПРИМЕЧАНИЕ Инициализаторы наследуются от суперкласса к подклассу.

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

ПРИМЕЧАНИЕ Инициализатор может устанавливать значения констант.

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

Вспомогательные инициализаторы

Помимо назначенных, в Swift существуют вспомогательные (convenience) инициализаторы. Они являются вторичными и поддерживающими. Вы можете определить вспомогательный инициализатор для проведения настроек и обязательного вызова одного из назначенных инициализаторов. Вспомогательные инициализаторы не являются обязательными для их реализации в типе. Создавайте их, если это обеспечивает наиболее рациональный путь решения поставленной задачи.

Синтаксис объявления вспомогательных инициализаторов не слишком отличается от синтаксиса назначенных.

Синтаксис

convenience init(параметры) {

    // тело инициализатора

}

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

В теле инициализатора обязательно должен находиться вызов одного из назначенных инициализаторов.

Вернемся к иерархии определенных ранее классов Quadruped, Dog и NoisyDog. Давайте перепишем класс Dog таким образом, чтобы при установке он давал возможность выводить на консоль произвольный текст. Для этого создадим вспомогательный инициализатор, принимающий на входе значение для наследуемого свойства type (лис­тинг 29.1).

Листинг 29.1

class Dog: Quadruped {

    //метод из листинга 5

    override init(){

        super.init()

        self.type = "dog"

    }

    //инициализатор листинга 1 со страницы Init

    convenience init(text: String){

         self.init()

         print(text)

    }

    func bark(){

        print("woof")

    }

    // метод из листинга 2

    func printName(){

        print(self.name)

    }

}

var someDog = Dog(text: "Экземпляр класса Dog создан")

В результате при создании нового экземпляра класса Dog вам будет предложено выбрать один из двух инициализаторов: init() или init(text:). Вспомогательный инициализатор вызывает назначенный инициализатор и выводит текст на консоль.

ПРИМЕЧАНИЕ Вспомогательный инициализатор может вызывать назначенный инициализатор через другой вспомогательный инициализатор.

Наследование инициализаторов

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

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

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

Отношения между инициализаторами

В вопросах отношений между инициализаторами Swift соблюдает следующие правила:

• Назначенный инициализатор подкласса должен вызвать назначенный инициализатор суперкласса.

• Вспомогательный инициализатор должен вызвать назначенный инициализатор того же объектного типа.

• Вспомогательный инициализатор в конечном счете должен вызвать назначенный инициализатор.

Иллюстрация всех трех правил представлена на рис. 29.1.

Рис. 29.1. Отношения между инициализаторами

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

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

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

В некоторых ситуациях бывает необходимо определить объектный тип, создание экземпляра которого может закончиться неудачей, вызванной некорректным набором внешних параметров, отсутствием какого-либо внешнего ресурса или иным обстоятельством. Для этой цели служат проваливающиеся (failable) инициализаторы. Они способны возвращать nil при попытке создания экземпляра. И это их основное предназначение.

Синтаксис

init?(параметры) {

    // тело инициализатора

}

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

В теле инициализатора должно присутствовать выражение return nil.

Рассмотрим пример реализации проваливающегося инициализатора. Создадим класс, описывающий сущность «прямоугольник». При создании экземпляра данного класса необходимо контролировать значения передаваемых параметров (высота и ширина), чтобы они обязательно были больше нуля. При этом в случае некорректных значений параметров программа не должна завершаться с ошибкой.

Для решения данной задачи используем проваливающийся инициализатор (листинг 29.2).

Листинг 29.2

class Rectangle{

    var height: Int

    var weight: Int

    init?(height h: Int, weight w: Int){

        self.height = h

        self.weight = w

        if !(h > 0 && w > 0) {

            return nil

        }

    }

}

var rectangle = Rectangle(height: 56, weight: -32) // возвращает nil

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

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

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

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

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

Листинг 29.3

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")

При создании экземпляра перечисления в качестве входного параметра symbol передается значение. На основе переданного значения возвращается соответствующий член перечисления.

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

Листинг 29.4

enum TemperatureUnit: Character {

    case Kelvin = "K", Celsius = "C", Fahrenheit = "F"

}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")

fahrenheitUnit!.hashValue

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

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

Обязательные инициализаторы

Обязательный (required) инициализатор — это инициализатор, который обязательно должен быть определен во всех подклассах данного класса.

Синтаксис

required init(параметры) {

    // тело инициализатора

}

Для объявления обязательного инициализатора перед ключевым словом init указывается модификатор required.

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

При реализации инициализатора в подклассе ключевое слово override не используется.

29.2. Деинициализаторы

Деинициализаторы являются отличительной особенностью классов.

Деинициализатор автоматически вызывается во время уничтожения экземпляра класса. Вы не можете вызвать деинициализатор самостоятельно. Один класс может иметь максимум один деинициализатор.

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

Синтаксис

deinit {

     // тело деинициализатора

}

Для объявления деинициализатора предназначено ключевое слово deinit, после которого в фигурных скобках указывается тело деинициализатора.

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

Рассмотрим пример использования деинициализатора (листинг 29.5).

Листинг 29.5

class SuperClass {

    init?(isNil: Bool){

        if isNil == true {

            return nil

        }else{

            print("Экземпляр создан")

        }

    }

    deinit {

        print("Деинициализатор суперкласса")

    }

}

class SubClass:SuperClass{

    deinit {

        print("Деинициализатор подкласса")

    }

}

var obj = SubClass(isNil: false)

obj = nil

Консоль

Экземпляр создан

Деинициализатор подкласса

Деинициализатор суперкласса

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

Назад: 28. Псевдонимы Any и AnyObject
Дальше: 30. Удаление экземпляров и ARC