swift

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

Введение

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

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

Базовые инициализаторы

Простейший инициализатор

class Person {
    var name: String
    var age: Int
    
    // Простой инициализатор
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let person = Person(name: "Виктория", age: 19)

В данном инициализаторе нет никакой дополнительной логики, он лишь запрашивает значения для полей name и age

Инициализатор по умолчанию

class Car {
    var brand = "Unknown"
    var year = 2024
    
    // Инициализатор не нужен так как нет полей с незаданными значениями
}

let car = Car() // brand = "Unknown", year = 2024

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

В таком случае init нам не требуется, но вместе с тем – никто не запрещает нам его добавить!

Инициализаторы с параметрами по умолчанию

class Rectangle {
    var width: Double
    var height: Double
    
    /// Если есть значение по умолчанию – данное значение можно не указывать
    init(width: Double = 1.0, height: Double = 1.0) {
        self.width = width
        self.height = height
    }
}

let square = Rectangle(width: 5.0, height: 5.0)
let defaultRect = Rectangle() // width = 1.0, height = 1.0

Данный инициализатор имеет немного дополнительной логики – мы указываем, что ширина и высота будут равны 1.0 если не указать иного.

Как вы могли бы заметить – square мы проинициализировали как (width: 5.0, height: 5.0), но для defaultRect мы не стали указывать никакого значения в инициализаторе и у нас они равны 1.0 (так как в init мы об этом указали)

Удобные инициализаторы (Convenience Initializers)

Удобным (convenience) он называется по той причине, что у нас всегда есть основной init(brand: String, year: Int) , который может быть довольно сложен, а мы можем добавить инициализатор попроще, но он всегда должен вызывать внутри себя основной!

class Vehicle {
    var brand: String
    var year: Int
    
    init(brand: String, year: Int) {
        self.brand = brand
        self.year = year
    }
    
    // удобный инициализатор
    convenience init(brand: String) {
        self.init(brand: brand, year: 2024)
    }
    
    convenience init() {
        self.init(brand: "Unknown")
    }
}

let vehicle1 = Vehicle(brand: "Toyota", year: 2020)
let vehicle2 = Vehicle(brand: "Honda") // year = 2024
let vehicle3 = Vehicle() // brand = "Unknown", year = 2024

Обратите внимание – упрощённый инициализатор convenience init() внутри себя вызывает основной self.init(brand: "Unknown") и указывает значение бренда как "Unknown"

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

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

Инициализаторы, которые могут вернуть nil при неудачной инициализации.

class Person {
    var name: String
    var age: Int
    
    init?(name: String, age: Int) {
        guard age >= 0 else { return nil }
        guard !name.isEmpty else { return nil }
        self.name = name
        self.age = age
    }
}

if let person = Person(name: "Анна", age: 25) {
    print("Person создан: \(person.name)")
}

let invalidPerson = Person(name: "", age: 25) // nil
let invalidAge = Person(name: "Анна", age: -5) // nil

Обратите внимание – init? означает, что у нас может и не получится создать объект, например если указан отрицательный возраст – то в init мы вернём nil и объект не создастся.

Инициализаторы структур

Структуры автоматически получают инициализатор.

struct Point {
    var x: Double
    var y: Double
}

let point = Point(x: 10.0, y: 20.0) // автоматический инициализатор (Мы не видим его внутри структуры, но он есть)

Это было бы полностью идентично, если бы мы написали слеудющее:

struct Point {
    var x: Double
    var y: Double

	// Данный идентификатор уже есть в Структуре по умолчанию
	init(x: Double, y: Double) {
		self.x = x
		self.y = y
	}
}

Логика в инициализаторе

До сих пор мы рассматривали примеры, в который практически отсутствовала логика в инициализаторе – мы лишь прямолинейно указывали значение для всех требуемых полей и вы, верно задаётесь вопросом

и это всё ?

Нет, инициализаторы способны на многое – их задача состоит из следующих пунктов:

  1. Проинициализировать значение всех полей
  2. Подготовить данные, если есть такая необходимость
  3. Запустить функции, которые должны начать свою работу именно в тот момент, как только объект класса создан.
struct Size {
    var width: Double
    var height: Double

	/// Специальный инициализатор для квадрата 
	init(side: Double) {
        self.width = side
        self.height = side

		/// Запускаем функцию, в ту же секунду как объект проинициализировался
        calculateSquare()
    }

	func calculateSquare() {
	    print("Высчитываем площадь квадрата: \(width * height)")
	}
}

let square = Size(side: 10.0) // "Высчитываем площадь квадрата: 100"

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

Это пример инициализатора, который упрощает создание именно квадрата – нам достаточно указать лишь одну из сторон, в сам инициализатор уже позаботится о том, чтобы каждое из полей имело значение self.width = side и self.height = side

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

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

Это прямая противоположность инициализатору. Если инициализатор это логика в момент создания), то деинициализатор это логика в момент уничтожения объекта класса.

Используется не часто, но нужно понимать логику.

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

class FileHandler {
    var filename: String
    
    /// Знакомый уже нам инициализатор
    init(filename: String) {
        self.filename = filename
        print("Файл \(filename) открыт")
    }
    
    /// Как только объект класса будет уничтожен – автоматически вызовется
    deinit {
        print("Файл \(filename) закрыт")
        // очистка ресурсов, закрытие файлов и т.д.
    }
}

var handler: FileHandler? = FileHandler(filename: "data.txt")

/// Как только мы скажем, что объект класса – nil, он будет уничтожен и последнее, что он сделает – вызовев deinit
handler = nil // "Файл data.txt закрыт"

Пример с банковским счетом

class BankAccount {
    let accountNumber: String
    var balance: Double
    
    /// Инициализатор
    init(accountNumber: String, initialBalance: Double) {
        self.accountNumber = accountNumber
        self.balance = initialBalance
        print("Счет \(accountNumber) создан. Баланс: \(balance)")
    }
    
    /// Деинициализатор
    deinit {
        print("Счет \(accountNumber) закрыт. Финальный баланс: \(balance)")
        // отправка финального отчета, закрытие соединений
    }
}

func testAccount() {
    let account = BankAccount(accountNumber: "12345", initialBalance: 1000)
    account.balance -= 500
    // при выходе из функции счет будет закрыт
}

testAccount()
// Счет 12345 создан. Баланс: 1000
// Счет 12345 закрыт. Финальный баланс: 500

Обратите внимание: так как мы создали BankAccount внутри функции – система понимает, что как только функция отработает его нужно уничтожить так как в нём уже нет смысла.

Дело в том, что всякий раз, когда мы создаём объект (let account = BankAccount(accountNumber: "12345", initialBalance: 1000)) он занимает место в оперативной памяти телефона.

Нам же нужно хранить данные всех полей ?

У нас тысячи функций, десятки тысяч созданных объектов, если бы они не самоуничтожались, как только в них теряется смысл – у нас бы закончилось всё доступное место.

И как только реальный объект будет уничтожен, на последнем издыхании он скажет deinit.........

Заключение

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