swift

Вычисляемые и ленивые свойства в Swift 🧮

Что такое вычисляемые свойства?

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

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

Чем-то они очень похожи на функции, но главное отличие – вычисляемы свойства не могут принимать параметры, они могут работать только с полями внутри класса или структуры.

Пример вычисляемого свойства 🌡️

Допустим, у нас есть температура в цельсиях, а мы хотим получать ещё и в фаренгейтах:

struct Temperature {
    var celsius: Double
    
    // Вычисляемое свойство
    var fahrenheit: Double {
        return celsius * 9 / 5 + 32
    }

	// Аналогичная функция
    func fahrenheit() -> Double {
         return celsius * 9 / 5 + 32
    }
}

let temp = Temperature(celsius: 25)
print(temp.celsius)      // 25
print(temp.fahrenheit)   // 77.0
print(temp.fahrenheit()) // 77.0

Обрати внимание: мы нигде не храним fahrenheit, он каждый раз пересчитывается из celsius

Синтаксис вычисляемых свойств 📝

Вычисляемое свойство всегда объявляется как var и после него идёт блок кода в фигурных скобках:

var fullName: Тип {
    name + " " + lastName
}

Можно (и нужно) опускать return, если код состоит из одного выражения – так лаконичнее

Зачем нужны вычисляемые свойства? 🤔

1. Требуемая логика может быть посчитана на основе существующих полей

Самое частое использование — когда одно значение получается из других:

struct Person {
    var firstName: String
    var lastName: String
    var fathersName: String
    
    // Полное имя
    var fullName: String {
        "\(firstName) \(lastName)"
    }
    
    // Инициалы
    var initials: String {
        let name = firstName.first?.uppercased() ?? ""
        let last = lastName.first?.uppercased() ?? ""
        let father = fathersName.first?.uppercased() ?? ""
        return "\(name).\(last).\(father)."
    }
}

let person = Person(firstName: "Хавронья", lastName: "Усатова", fathersName: "Йосифовна")
print(person.fullName)  // Хавронья Ульянова
print(person.initials)  // Х.У.Й.

Мы могли добавить отлельные поля для полного имени и инициалов, но зачем ?

Ведь их мы можем вычислить из firstName и lastName и не утруждать пользователя заставляя его указывать лишнюю информацию.

А теперь про ленивые свойства (Lazy Properties) 🦥

Ленивые свойства — это полная противоположность вычисляемым.

Дело в том, что вычисляемые поля – пересчитывают значение всякий раз как мы к ним обращаемся, а вот ленивые – только один раз при первом обращении к ним ни раньше и не позже.

Когда это нужно?

Представь, что у тебя есть свойство, которое:

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

Простейший пример ленивого свойства 🏋️‍♂️

struct PiNumber {
    lazy var calculatePi: String = {
        return "Очень сложные вычисления числа Пи, которые занимают огромное время"
    }()
}

var piNumber = PiNumber()   // Мы создали наш объект, но не вызываем calculatePi
print(piNumber.calculatePi) // 3.141592653589793238462643383279502884197169399375

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

Мы произведём расчёты только один раз, так как число Пи не меняется.

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

Достаточно посчитать его один раз.

В этом и преимущество вычисляемого поля – оно будет посчитано лишь однажды, а затем будет возвращать уже посчитанное значение.

Но если значение может измениться при вычислении – необходимо использовать вычисляемое поле.

Хавронья могла, наконец, выйти замуж, как минимум по причине необходимости улучшить свои инициалы, и в приложении она может изменить свою фамилию и теперь она стала не “Усатова” а “Уткина”, что в прочем не сильно улучшило её инициалы.

Если бы поле fullName у нас было бы помечено как lazy – то оно бы не стало пересчитывать по новой новую фамилию, а всё так же использовала старую (ту с которой была вызвана в первый раз)

var fullName: Тип {
    name + " " + lastName
}

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

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

Ленивые свойства при инициализации класса (структуры) 🧩

Какое есть преимущества у ленивых свойств помимо того, что они вычислятся лишь один раз ?

Важное преимущество ленивых свойство – они вообще не начинают высчитываться пока мы их об этом не попросим (поэтому они и ленивые)

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

struct Settings {
    let userDefaults = UserDefaults()
    let dateFormatter = DateFormatter()
}

Но что если таких свойств у нас много и их очень сложно создавать (например внутри DateFormatter есть ещё с десяток полей, которые тоже начнут создаваться и так по цепочке)

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

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

UserDefaults это хранилище, в котором мы можем хранить информацию, даже когда приложение закрыто.

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

struct Settings {
    lazy var userDefaults: UserDefaults = {
        print("📦 Создаём UserDefaults")
        return UserDefaults.standard
    }()
    
    lazy var dateFormatter: DateFormatter = {
        print("📅 Создаём DateFormatter")
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        return formatter
    }()
}

// Мы создали Settings, но все ленивые поля так и не созданы.
var settings = Settings()

// Только теперь наш UserDefaults создастся – когда мы попытаемся в него, что-то сохранить
print(settings.userDefaults.set("password123", forKey: "userPass")))

Это очень удобно для тяжёлых объектов, которые не всегда нужны.

Ленивые свойства в структурах и классах 🔄

class Database {
    lazy var connection = {
        print("🔌 Подключаемся к БД")
        return "Connection"
    }()
}

let db = Database()
print("Объект создан")
print(db.connection) // Подключается только сейчас, а не в момент создания Database

В структурах

Нужно быть внимательным — доступ к lazy требует mutating: (xCode сам об этом подскажет –не тратьте ресурсы мозга на запоминание )

struct FileProcessor {
    lazy var fileHandle: FileHandle? = {
        print("📂 Открываем файл")
        return FileHandle(forReadingAtPath: "Epstein.pdf")
    }()
    
    mutating func readData() {
        guard let handle = fileHandle else { return }
        // читаем данные...
    }
}

var processor = FileProcessor()
processor.readData() // Здесь файл откроется

Чтение файла занимает время, пользователь может быть вообще не собирается его читать – в таком случае fileHandle выгодно сделать как lazy – ленивое поле, начнёт вычислять только, когда оно действительно понадобится (в момент его вызова)

Где применяются ленивые свойства? 🎯

1. Кэширование сложных вычислений

struct Calculator {
    var numbers: [Int]
    
    /// Подсчёт среднего значения в массиве
    lazy var average: Double = {
       // reduce – Функция, которая складывает все значения в массиве numbers
       // 0 – точка отсчёта
       // + значит, что их нужно именно сложить, а не разделить (/) вычесть (-) или умножить (*)
        let sum = numbers.reduce(0, +) 
        return Double(sum) / Double(numbers.count)
    }()
    
    /// Сложная функция сортировки
    lazy var sortedNumbers: [Int] = {
        numbers.sorted()
    }()
}

var calc = Calculator(numbers: [5, 3, 8, 1, 9])
print(calc.average) // считает один раз. Затрачено время
print(calc.average) // берёт из кэша. Моментально
print(calc.average) // берёт из кэша. Моментально

2. Ресурсы, которые могут не понадобиться

struct ReportGenerator {
    lazy var reportData: Data = {
        // Чтение гигантского файла
        return Data(contentsOf: URL(fileURLWithPath: "report.pdf"))
    }()
    
    lazy var charts: [Chart] = {
        // Построение сложных графиков
        return []
    }()
}

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

Сравнение: вычисляемые vs ленивые vs обычные 📊

Обычные поля Вычисляемые Ленивые
Занимают память Не занимают Занимают после вычисления
Создаются сразу Считаются каждый раз Создаются при первом обращении
Могут быть let Только var Только var

Когда что использовать? 🎯

Используй обычные хранимые свойства, если:

Используй вычисляемые свойства, если:

Используй ленивые свойства, если:

Итоги 🎯

Вычисляемые свойства:

Ленивые свойства:

Золотые правила

“Если значение можно вычислить — сделай его вычисляемым свойством.” “Если объект тяжёлый и может не понадобиться — сделай его ленивым.”