JavaRush /Курси /Swift SELF /Обчислювані властивості у struct: get/set

Обчислювані властивості у struct: get/set

Swift SELF
Рівень 24 , Лекція 0
Відкрита

1. Обчислювані властивості: навіщо вони потрібні й що це таке

Коли ви починаєте писати власні struct, дуже швидко виникають дві потреби. Перша — показати гарну інформацію про стан, наприклад: «книга: 1984, автор: …». Друга — зберігати похідні значення, скажімо, «скільки книг зараз у бібліотеці?», так, щоб вони не втрачали синхронність.

Обчислювані властивості — це саме про такі випадки: вони дають змогу читати, а іноді й записувати значення через зрозумілу назву властивості, хоча всередині воно обчислюється або перенаправляє запис до інших полів.

Уявіть, що struct — це маленький організм. Збережені властивості — це його «органи», а обчислювані властивості — це його «аналізи»: за станом організму можна швидко оцінити тиск, пульс і рівень кофеїну в крові, особливо перед дедлайнами, але зберігати ці значення окремо небезпечно — забудете оновити, і дані роз’їдуться.

Збережені vs обчислювані: «лежить у памʼяті» vs «обчислюється під час звернення»

Перш ніж переходити до обчислюваних властивостей, важливо чітко розвести два поняття.

Збережена властивість реально зберігає значення всередині екземпляра struct. Це «ящик», у якому лежить число або рядок.
Обчислювана властивість — це, по суті, функція, замаскована під властивість: під час звернення Swift запускає код, який повертає результат, а іноді ще й приймає нове значення та змінює базовий стан.

Зручно тримати в голові просту схему:

flowchart LR
  A[Збережені властивості
єдине джерело істини] --> B[Обчислювані властивості
похідні значення] B --> C[UI/друк/логіка]

Якщо джерело істини одне (stored properties), то обчислювані властивості стають «вітринами» й «калькуляторами», які щоразу повертають актуальне значення.

Обчислювана властивість лише для читання: найчастіший випадок

Найчастіше обчислювана властивість потрібна лише для читання: щоб вивести, перевірити або порахувати значення. У такому випадку ви робите обчислювану властивість лише для читання — без set.

У Swift це пишеться дуже компактно: якщо всередині лише читання, можна не писати get, а повернути вираз напряму.

Мініприклад про прямокутник:

struct Rectangle {
    var width: Int
    var height: Int

    var area: Int { // обчислювана, лише для читання
        width * height
    }
}

let r = Rectangle(width: 3, height: 4)
print(r.area) // 12

Тут area ніде не зберігається. Воно щоразу обчислюється з width і height, тому не може «забути оновитися».

Тепер перенесімо ідею на «книгу» — похідне значення з двох полів:

struct BookCard {
    var title: String
    var authorFirstName: String
    var authorLastName: String

    var authorFullName: String { // похідне значення
        "\(authorFirstName) \(authorLastName)"
    }
}

let b = BookCard(title: "Dune", authorFirstName: "Frank", authorLastName: "Herbert")
print(b.authorFullName) // Frank Herbert

authorFullName — ідеальний кандидат на обчислювану властивість: зберігати повне імʼя окремо безглуздо, бо воно на 100% виводиться з двох полів.

2. Обчислювана властивість із get/set: альтернативний інтерфейс до тих самих даних

Іноді обчислювана властивість потрібна не лише для читання, а й для запису. Важливо розуміти ідею: коли у обчислюваної властивості є set, це майже завжди означає, що вона стає альтернативною точкою входу для зміни базових збережених властивостей.

Тобто ми не «зберігаємо» значення обчислюваної властивості — ми даємо змогу присвоювати його, а всередині перетворюємо це присвоєння на зміну реальних полів.

У стандартній моделі обчислюваних властивостей у Swift є аксесор читання (getter), який повертає значення під час читання, і необовʼязковий аксесор запису (setter), який приймає нове значення. За замовчуванням воно доступне як newValue.

Приклад: секунди ↔ хвилини

struct Duration {
    var seconds: Int

    var minutes: Int {
        get { seconds / 60 }
        set { seconds = newValue * 60 }
    }
}

var d = Duration(seconds: 90)
print(d.minutes) // 1
d.minutes = 2
print(d.seconds) // 120

Ззовні здається, що ми «записали хвилини». Але насправді ми змінили seconds. Саме в цьому й сенс: обчислювана властивість із set часто працює як «перекладач» або «адаптер».

Приклад: імʼя автора як одне поле, але зберігається як два

Зробімо окремий тип AuthorName, щоб не розпорошувати поля по різних місцях:

struct AuthorName {
    var first: String
    var last: String

    var full: String {
        get { "\(first) \(last)" }
        set {
            let parts = newValue.split(separator: " ", maxSplits: 1)
            first = parts.first.map(String.init) ?? ""
            last = (parts.count > 1) ? String(parts[1]) : ""
        }
    }
}

var a = AuthorName(first: "Mary", last: "Shelley")
print(a.full) // Mary Shelley
a.full = "Arthur Conan Doyle"
print("\(a.first), \(a.last)") // Arthur, Conan Doyle

Дві деталі, які корисно зафіксувати:

  • У set вам не потрібно оголошувати параметр вручну — newValue дається автоматично.
  • Оскільки ми змінюємо first і last, екземпляр має бути var. Якщо написати let a = AuthorName(...), то a.full = "..." не скомпілюється: let забороняє мутацію стану struct.

3. Нормалізація даних у setter: «гігієна» для стану

Ще один практичний сценарій: ви хочете, щоб зовнішній код міг писати у властивість так, як йому зручно, а всередині ви приводили значення до охайного вигляду — прибирали зайві пробіли, відсікали порожні рядки, уніфікували формат.

Люди вводять дані так, як уміють, тобто не надто передбачувано, тому нормалізація на вході часто робить тип значно надійнішим.

Приклад: зберігатимемо «сире» значення в rawTitle, а назовні віддаватимемо чисте title.

import Foundation

struct BookTitle {
    private var rawTitle: String

    var title: String {
        get { rawTitle }
        set { rawTitle = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
    }

    init(title: String) {
        self.rawTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

var b = BookTitle(title: "   Clean Code   ")
print(b.title) // Clean Code
b.title = "  Dune\n"
print(b.title) // Dune

Тут обчислювана властивість робить важливу річ: вона гарантує єдиний формат і зберігає одне джерело істини (rawTitle) за зручного інтерфейсу для зовнішнього коду (title).

4. Пастки й читабельність обчислюваних властивостей

Пастка: рекурсія обчислюваної властивості

Коли ви тільки починаєте, дуже легко написати обчислювану властивість, яка звертається… до самої себе. У цей момент ви створюєте нескінченний виклик без базового випадку: спробу обчислити значення, потрібне для обчислення значення, потрібного для обчислення значення…

Поганий приклад (так робити не можна):

struct BadBook {
    var title: String {
        title // ❌ рекурсія: getter викликає сам себе
    }
}

Правильний спосіб — мати сховище (stored property), наприклад rawTitle, і обчислювати значення з нього:

struct GoodBook {
    private var rawTitle: String

    var title: String { rawTitle } // обчислювана читає збережене

    init(title: String) {
        self.rawTitle = title
    }
}

Правило читабельності: обчислювана властивість — не місце для «роману»

Обчислювані властивості легко полюбити, і тут зʼявляється небезпека: почати вкладати туди взагалі всю логіку світу. У результаті читач коду очікує від book.shortDescription простого доступу, а отримує «маленький бізнес-процес».

Практичне правило для початківців таке: обчислювана властивість має бути простою й передбачуваною. Якщо вам потрібно виконати дію, яка більше схожа на операцію, краще винести її в метод.
Властивість — це «що це таке?», метод — «що зробити?».

Короткий і доречний варіант:

struct BookInfo {
    var title: String
    var author: String
    var year: Int

    var shortDescription: String {
        "\(title) — \(author) (\(year))"
    }
}

Якщо всередині обчислюваної властивості починають зʼявлятися довгі умови, цикли й «розбір рядка на 50 випадків», чесніше зазвичай винести це в метод:

struct BookInfo {
    var title: String
    var author: String
    var year: Int

    func shortDescription(withYear: Bool) -> String {
        withYear ? "\(title) — \(author) (\(year))" : "\(title) — \(author)"
    }
}

Рамка: що обовʼязково зрозуміти зараз, а що можна залишити на потім

У цій темі є кілька рівнів глибини. Якщо спробувати «зʼїсти» все одразу, мозок влаштує fatalError() (жарт… але не надто). Нижче — добра рамка для першого проходу.

Обовʼязково зрозуміти зараз Можна відкласти
Обчислювана властивість не зберігає значення, а обчислює його під час звернення Детальніші моделі доступу до властивостей і оптимізаційні нюанси
Як писати обчислювану властивість лише для читання компактно (var x: T { ... }) Рідкісні синтаксичні аксесуари та вузькі випадки
Як працюють get і set, і що таке newValue Нюанси продуктивності та деталі ABI у великих бібліотеках
Чому обчислювана властивість не має посилатися на саму себе (рекурсія) Спроби перетворити обчислювану властивість на «міні-API» і чому це зазвичай шкідливо
Setter змінює збережений стан, а отже потрібен var-екземпляр Реактивність і «стеження за змінами» — це окремі великі теми

5. Мініприклад: Library зі статистикою без дублювання

Щоб відчути користь обчислюваних властивостей на рівні Library, зробімо маленький Library, який зберігає книги й повертає похідні значення без додаткового зберігання.

Ми поки що не переходимо до файлів, мережі та архітектури — просто тримаємо дані в памʼяті.

struct Library {
    var books: [BookInfo]

    var totalCount: Int { books.count }

    var classicsCount: Int {
        books.filter { $0.year < 1970 }.count
    }
}

let lib = Library(books: [
    BookInfo(title: "Dune", author: "Frank Herbert", year: 1965),
    BookInfo(title: "Clean Code", author: "Robert C. Martin", year: 2008)
])

print(lib.totalCount)     // 2
print(lib.classicsCount)  // 1

Зверніть увагу: ми ніде не зберігаємо totalCount і classicsCount. І це добре. Інакше вам довелося б памʼятати: «я додав книгу — не забудь оновити два лічильники». А людська памʼять — річ чудова, але не для узгодженості даних.

6. Типові помилки під час роботи з обчислюваними властивостями

Помилка №1: зберігати похідне значення як збережену властивість і забувати його оновлювати.
Наприклад, тримати booksCount окремим полем і змінювати його вручну під час кожного append/remove. Поки код маленький, усе працює. Потім ви додаєте ще один спосіб змінювати масив, забуваєте підправити лічильник — і дані стають недостовірними. Якщо значення виводиться з інших полів, йому найчастіше місце в обчислюваній властивості.

Помилка №2: рекурсивний getter або setter.
Ситуація «var title: String { title }» виглядає майже як описка, але за змістом це нескінченний виклик самого себе. Розвʼязання просте: зберігайте дані в іншій збереженій властивості, часто з назвою на кшталт rawTitle/storage, і обчислюйте результат із неї.

Помилка №3: занадто важка логіка всередині обчислюваної властивості.
Коли в обчислюваній властивості зʼявляються довгі умови, цикли, «розбір рядка на 50 випадків», код починає сприйматися як дія, а не як властивість. Тоді читач коду очікує від book.shortDescription простого доступу, а отримує «маленький бізнес-процес». Винесення в метод робить намір чеснішим.

Помилка №4: неочікувана поведінка set, яка змінює багато неповʼязаних полів.
Setter обчислюваної властивості має бути зрозумілим: він дає альтернативний спосіб записати ті самі дані. Якщо присвоєння в одну властивість раптом змінює багато неповʼязаних полів, зовнішній код починає жити у світі сюрпризів. У доменній моделі сюрпризи зазвичай шкодять.

Помилка №5: спроба змінити обчислювану властивість у let-екземпляра.
Якщо обчислювана властивість має set, то присвоєння — це мутація стану. Отже, екземпляр має бути var. Це не «примха Swift», а захист: let каже «цей об’єкт незмінний», і мова тримає слово.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ