Инициализаторы в Swift: подробное руководство

В Swift инициализатор – это специальная функция init(), которая используется для создания объектов определенного класса, структуры или перечисления.

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

Что такое инициализатор в Swift?

В Swift инициализатор – это специальная функция, которую мы используем для создания объектов определенного класса, структуры или перечисления. Инициализаторы иногда называют конструкторами , потому что они «конструируют» объекты.

Давайте посмотрим на пример. Создадим структуру с именем Book, которая имеет 3 свойства: title, author и pages.

struct Book {
    var title = ""
    var author = ""
    var pages = 0
}

Создадим экземпляр структуры Book:

var lotr = Book()
print(lotr.pages)
// 0

В приведенном выше коде первая строка использует инициализатор Book() для создания объекта типа Book. Можно сказать, что мы «инициализировали» объект Book и присвоили его переменной lotr.

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

Как мы можем настроить способ работы инициализатора?

struct Book {
    var title = ""
    var author = ""
    var pages = 0
 
    init(title: String) {
       self.title = title
    }
  • Внутри класса или структуры инициализатор имеет специальное имя – init().
  • Приведенный выше инициализатор имеет один параметр title типа String.
  • Внутри функции мы присваиваем значение параметра title свойству title.
  • Код self относится к свойству Book.

Вне структуры вы используете Book() для инициализации объекта Book. Внутри структуры вы используете функцию init() для настройки инициализации объекта Book.

struct Book {
    var title = ""
    var author = ""
    var pages = 0
 
    init(title: String) {
        self.title = title
    }
}
 
var lotr = Book(title: "Братство Кольца")
lotr.pages = 479
lotr.author = "Толкин"
 
print(lotr.title)
// Братство кольца

Давайте рассмотрим несколько типов инициализаторов, которые мы можем использовать:

  • Встроенный инициализатор, который будет инициализировать объект со значениями свойств по умолчанию.
  • Поэлементный (memberwise) инициализатор для структур, который будет автоматически инициализировать свойства структуры.
  • Failable инициализаторы, который могут возвращать nil на основании параметров, предоставленных инициализатору.

Встроенный инициализатор

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

class Car {
    var name = ""
}
 
let tesla = Car()
инициализатора.name = "Tesla"

В приведенном выше коде мы создали класс Car со свойством name типа String. Далее мы инициализируем экземпляр класса Carс помощью инициализатора Car() и присваиваем объект переменной tesla. Мы также устанавливаем новое значение для name.

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

Мы также можем использовать следующий синтаксис:

class Car {
    var name: String
 
    init() {
        self.name = ""
    }
}

У свойства name теперь есть явная аннотация типа String. Функция init() содержит код для присвоения значения по умолчанию для свойства name. То есть в данном случае мы разделяем объявление свойства и его инициализацию.

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

Поэлементный инициализатор

Поэлементный (memberwise) инициализатор доступен только для структур. Swift будет автоматически синтезировать инициализатор для структур на основании свойств этой структуры, если вы не предоставите собственных инициализаторов init( ).

struct Rectangle {
    var width = 0
    var height = 0
}
 
let square = Rectangle(width: 10, height: 10)

У нас есть структура Rectangle с двумя свойствами width и height типа Int со значениями по умолчанию 0. У нас есть возможность инициализировать экземпляр Rectangle с помощью поэлементного инициализатора.

Инициализатор Rectangle(width:height:) генерируется автоматически. Если мы бы сами написали данный инициализатор, он бы выглядел следующим образом:

init(width: Int = 0, height: Int = 0) {
    self.width = width
    self.height = height
}

Следует помнить, что:

  • Поэлементный инициализатор работает только для структур, а не для классов.
  • Если вы создадите свой собственный инициализатор с помощью функции init(), вы потеряете поэлементный инициализатор.

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

let square = Rectangle()
square.width = 10
print(square.height) // 0

Failable инициализаторы

Failable инициализатор – это инициализатор, который может вернуть какое-либо значение или может вернуть nil. Это своего рода иниализатор для опционалов.

Рассмотрим следующий код:

let age = Int("42")
print(age)
// Optional(42)
 
let year = Int("two thousand and twenty")
print(year)
// nil

Инициализатор Int(···) – failable. Это означает, что он может вернуть nil, если вы предоставите неверные входные параметры.

В первом примере мы предоставляем строку “42”, которая может быть успешно преобразована в целочисленное значение 42. Во втором примере мы предоставляем текстовую строку, которая не может быть преобразована в целочисленное значение, поэтому year будет равен nil. Обратите внимание, что в обоих случаях тип возвращаемого значения – Int?.

Вы можете объявить создать собственный failable инициализатор с помощью конструкции init?():

init?(value: ···) {
    guard условие else {
        return nil
    }
 
    ···
}

В приведенном выше примере кода мы возвращаем nil, если наше условие возвращает false.

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

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

class Vehicle {
    required init() {
        // Этот инициализатор должен быть реализован в подклассе
    }
}

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

class Car: Vehicle {
    required init() {
        // Инициализатор
    }
}

Интересно, что ключевое слово required подразумевает, что реализация инициализатора подкласса фактически переопределена. Модификатор override для функции init() не требуется.

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

Начнем с кода для класса Circle:

class Circle {
    var radius: Double
    var circumference: Double {
        2 * .pi * radius
    }
 
    init(radius: Double) {
        self.radius = radius
    }
}

В приведенном выше коде мы определяем класс с именем Circle. Он имеет одно хранимое свойство radius типа Double и одно вычисляемое свойство circumference типа Double. Класс также имеет один назначенный инициализатор, объявленный как init(radius:), который присваивает параметр radius свойству с тем же именем.

Иными словами, мы можем создать круг, указать его радиус и получить окружность:

let earth = Circle(radius: 6371.0)
print(earth.circumference) 
// 40030.17

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

Объявим вспомогательный инициализатор:

convenience init(circumference: Double) {
    self.init(radius = circumference / (.pi * 2))
}

Приведенный выше инициализатор init(circumference:), называемый вспомогательным инициализатором, принимает параметр circumference. Внутри тела функции окружность круга используется для вычисления его радиуса, который предоставляется назначенному инициализатору init(radius:).

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

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

Почему мы просто не создали еще один назначенный инициализатор?

init(circumference: Double) {
    self.radius : circumference / (.pi * 2)
}

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

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

Прежде чем продолжить, обратите внимание на несколько правил для инициализаторов:

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

Давайте посмотрим, что 2-е правило в действии:

convenience init(circumference: Double) {
    self.init(radius : circumference / (.pi * 2))
}

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

Подклассы и инициализация

Определим подкласс Cylinder:

class Cylinder: Circle {
    var height: Double
    var volume: Double {
        .pi * radius * radius * height
    }
 
    init(radius: Double, height: Double) {
        self.height = height
        super.init(radius: radius)
    }
}

В приведенном выше коде мы объявили класс Cylinder который наследует класс Circle. Класс Cylinder наследует свойства и методы из класса Circle.

Также у класса Cylinder есть хранимое свойство height, а также вычисляемое свойство volume типа Double.

Класс Cylinder определяет один назначенный инициализатор, который принимает параметры radius и height. Параметр height назначается через self.height, а параметр radius нанзначается через вызов super.init(···)

Мы вызываем инициализатор суперкласса init(radius:), чтобы сохранить цепочку инициализаторов без изменений.

class Circle {
    var radius: Double
    var circumference: Double {
        2 * .pi * radius
    }
 
    init(radius: Double) {
        self.radius = radius
    }
 
    convenience init(circumference: Double) {
        self.init(radius: circumference / (.pi * 2))
    }
}
 
class Cylinder: Circle
{
    var height: Double
    var volume: Double {
        .pi * radius * radius * height
    }
 
    init(radius: Double, height: Double) {
        self.height = height
        super.init(radius: radius)
    }
}
 
let earth = Circle(radius: 6371.0)
print(earth.circumference)
 
let earth2 = Circle(circumference: 40030.17)
print(earth2.radius)
 
let pie = Cylinder(radius: 25.0, height: 5.0)
print(pie.volume)

Какие проблемы могут возникнуть с инициализаторами?

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

Необходимость вызывать назначенный инициализатор суперкласса

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

Рассмотрим классы Circle и Cylinder, которые мы создали ранее. Если мы добавим новый инициализатор в класс Cylinder, он должен вызвать назначенный инициализатор суперкласса Circle:

class Cylinder: Circle {
    ···
    init(volume: Double) {
        ···
        super.init(radius: ···)
    }    
}

init(radius:) на самом деле является назначенным инициализатором суперкласса Circle. Обязательно вызывайте назначенный инициализатор суперкласса.

В классе нет инициализаторов

Смысл этой ошибки прост: используемый вами класс не имеет инициализаторов. Обычно это означает 2 вещи:

  • Вы не задали свойств класса значения по умолчанию, что означает, что они не были проинициализированы.
  • Вы не добавили инициализатор init().
class Car {
    var name: String
}

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

Свойство не инициализировано при вызове super.init

Одно из правил работы с инициализаторами заключается в том, что назначенный инициализатор должен инициализировать каждое из своих свойств перед вызовом super.init(···).

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

Читайте также:
Комментарии (1)
  1. Спасибо тебе добрый человечек за статью. Только она и помогла мне разобраться в инициализаторах наследуемых классов.

Добавить комментарий

Ваш адрес email не будет опубликован.