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 каже «цей об’єкт незмінний», і мова тримає слово.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ