1. Синтаксис методів у struct
Коли ви тільки починаєте програмувати, здається природним тримати «дані» окремо, а «дії» — у зовнішніх функціях. Але доволі швидко код перетворюється на сцену з фільму жахів: функції розкидані по всьому проєкту, параметри всюди, а правила роботи з даними дублюються і губляться. Методи в struct розв’язують цю проблему: вони дозволяють тримати операції поруч із даними та робити код читабельним — як інструкцію, а не як загадку.
Уявіть, що в нас є книга в майбутньому консольному мінізастосунку «бібліотека» — ми поступово розвиватимемо його впродовж курсу. У книги є title, author і year. Без методів зовнішній код постійно повторюватиме одну й ту саму логіку: «сформуй рядок для друку», «перевір, чи підходить книга під запит», «онови назву книги». З методами це перетворюється на зрозумілі виклики: book.displayLine(), book.matches(query:), book.rename(to:). І читати такий код — майже як звичайний текст. Ну, майже: усе ж це програмування, тут без дивакуватостей ніяк.
Виклик через крапку і сигнатури
Методи в структурі пишуться майже так само, як звичайні функції, тільки живуть усередині фігурних дужок struct. Виклик методу відбувається через крапку: value.method(). Тут корисно запам’ятати важливу інтуїцію: метод завжди викликається «на якомусь конкретному екземплярі». Тобто метод — це не абстрактна дія, а дія, застосована до поточного об’єкта даних.
Почнімо з дуже маленького прикладу: структура книги й метод, який повертає гарний рядок для друку.
struct Book {
var title: String
var author: String
var year: Int
func displayLine() -> String {
return "\(title) — \(author) (\(year))"
}
}
let b = Book(title: "Dune", author: "Frank Herbert", year: 1965)
print(b.displayLine()) // Dune — Frank Herbert (1965)
Зверніть увагу: метод displayLine() не змінює книгу, він просто читає її властивості й повертає рядок. Тому його оголошують як звичайний func, без додаткових ключових слів.
Щоб не загубитися, тримайте в голові мінітаблицю «як читати сигнатуру»:
| Сигнатура методу | Що це означає людською мовою |
|---|---|
|
«Я не змінюю екземпляр, а обчислюю результат і повертаю його» |
|
«Я змінюю екземпляр — його властивості або навіть увесь self» |
self усередині методу
Слово self у методах Swift можна сприймати як «поточний екземпляр». Коли ви пишете b.displayLine(), усередині методу displayLine() змінна self вказує на b. Це схоже на «я» в розмові: у кожного мовця своє «я», і self — це «я» конкретного екземпляра.
У простих методах self часто можна не писати: Swift розуміє, що title означає self.title. Але іноді self стає корисним, щоб прибрати двозначність або підкреслити зміст.
struct Book {
var title: String
func shoutTitle() -> String {
return self.title.uppercased()
}
}
let b = Book(title: "Swift")
print(b.shoutTitle()) // SWIFT
Тут self.title і title майже однакові за змістом. Але якщо ви всередині методу зробите параметр із тим самим іменем, self стає не опцією, а рятівним кругом.
struct Book {
var title: String
mutating func rename(to title: String) {
self.title = title
}
}
var b = Book(title: "Swfit") // так, друкарська помилка
b.rename(to: "Swift")
print(b.title) // Swift
У цьому прикладі title — це параметр, а self.title — це властивість. Без self ви б присвоювали параметр самому собі, тобто робили б «нічого», але дуже впевнено.
3. Звичайні та mutating‑методи
func: читаємо дані й обчислюємо результат
Коли ви пишете метод без mutating, ви ніби обіцяєте: «Я не змінюю стан». Це дуже приємна обіцянка, бо вона зменшує кількість сюрпризів у коді. Такі методи особливо корисні для обчислень, форматування, перевірок і пошуку.
Додамо до книги метод «чи підходить під запит». Нехай поки це буде максимально проста логіка: перевіримо, чи містить назва або автор заданий підрядок. Без регулярних виразів і без нормалізації регістру — це все буде пізніше й в інших темах.
struct Book {
var title: String
var author: String
var year: Int
func matches(query: String) -> Bool {
return title.contains(query) || author.contains(query)
}
}
let b = Book(title: "Dune", author: "Frank Herbert", year: 1965)
print(b.matches(query: "Dune")) // true
print(b.matches(query: "Herb")) // true
print(b.matches(query: "Swift")) // false
Важливо відчути стиль: усі дії про книгу живуть у книзі. Ззовні ви не дублюєте title.contains(query) || author.contains(query) у різних місцях. Ви пишете book.matches(query:). Це здається дрібницею, але в довгостроковій перспективі саме такі дрібниці рятують психіку, коли проєкт зростає.
mutating: змінюємо stored properties
У Swift struct — це тип-значення. Це означає, що зміна екземпляра — це не побічний ефект десь у пам’яті, а зміна самого значення. Тому Swift вимагає, щоб ви явно позначали методи, які змінюють стан, ключовим словом mutating. Це зроблено не для того, щоб ускладнити життя новачкові, хоч на початку так і здається, а щоб у коді було видно: «ось тут відбувається зміна».
У стандартній бібліотеці багато прикладів: є методи, які повертають новий результат, а є методи, які змінюють колекцію «на місці». Наприклад, reverse() — це mutating‑метод, він розвертає колекцію на місці. У документації стандартної бібліотеки це прямо оформлюється як public mutating func reverse() для MutableCollection.
Спробуймо написати «бібліотечну» поведінку: наприклад, збільшити рік видання. Це безглуздо, зате добре демонструє мутацію — уявімо, що ми виправляємо помилку введення.
struct Book {
var title: String
var author: String
var year: Int
mutating func fixYear(to newYear: Int) {
year = newYear
}
}
var b = Book(title: "Dune", author: "Frank Herbert", year: 1964)
b.fixYear(to: 1965)
print(b.year) // 1965
Якщо прибрати mutating у fixYear, компілятор не дасть змінювати year усередині методу, бо ви намагаєтеся змінити стан типу-значення в методі, який формально нічого не змінює.
4. Корисні нюанси mutating
Можна замінити весь self
Зазвичай mutating використовують, щоб змінити одну або кілька збережуваних властивостей: balance += amount, items.append(x), x += 1. Але іноді корисно розуміти загальніший принцип: усередині mutating‑методу ви можете змінити увесь екземпляр цілком, присвоївши нове значення self.
Це виглядає трохи магічно, але насправді логічно: якщо struct — це значення, то змінити значення можна або по частинах, або цілком.
struct Counter {
var value: Int
mutating func reset() {
self = Counter(value: 0)
}
}
var c = Counter(value: 10)
c.reset()
print(c.value) // 0
Такий стиль буває корисним, коли ви хочете зробити скидання стану й водночас зберегти код коротким. Щоправда, якщо ви почнете зловживати self = ..., колеги дивитимуться на вас трохи насторожено, і в цьому буде раціональне зерно. Але як інструмент у вашому наборі — це корисно.
Чому mutating не можна викликати для let‑екземпляра
Тут студентам часто хочеться запитати: «Ну структура ж моя, чому Swift такий суворий?» Відповідь проста: let у Swift — це гарантія незмінності значення. Якщо екземпляр оголошено через let, ви пообіцяли компілятору: «Це значення більше не змінюється». А mutating‑метод якраз говорить: «Я змінюю значення». Ці обіцянки конфліктують, і компілятор обирає сторону безпеки.
У Swift це не теорія: навіть типи зі стандартної бібліотеки та Foundation проєктують так, щоб let справді «заморожував» об’єкт. Класичний приклад із обговорень змінюваності: Date як тип-значення логічно змінювати через mutating‑метод на var‑екземплярі, але забороняти на let‑екземплярі — і це сприймається як очікувана поведінка.
Подивімося на наш Book:
struct Book {
var title: String
mutating func rename(to newTitle: String) {
title = newTitle
}
}
let b = Book(title: "Old")
// b.rename(to: "New") // ❌ не можна: b оголошено через let
print(b.title) // Old
Якщо ви справді хочете змінювати книгу, оголошуйте екземпляр як var:
var b = Book(title: "Old")
b.rename(to: "New")
print(b.title) // New
Так, це виглядає як «зайве слово», але саме в цьому випадку зайве слово — чесна документація намірів.
Як швидко «прочитати» код і знайти мутацію
На невеликих програмах різниця між func і mutating func здається косметичною. Але в реальних проєктах це один зі способів не потонути в побічних ефектах. Коли ви бачите mutating, ви одразу розумієте: після виклику екземпляр може стати іншим. Коли mutating немає, ви можете спокійно викликати метод хоч десять разів поспіль, не боячись, що він щось тихо змінив.
Візуально цей процес зручно уявити такою схемою: виклик методу або завершується лише значенням, що повертається, або ще й новим станом.
flowchart TD
A["Є екземпляр struct (значення)"] --> B["Викликаємо метод"]
B --> C{"Метод mutating?"}
C -- "ні" --> D["Читаємо властивості → повертаємо результат"]
D --> E["Екземпляр залишається тим самим"]
C -- "так" --> F["Змінюємо властивості або self"]
F --> G["Екземпляр набуває нового значення"]
Якщо говорити зовсім по-людськи: mutating — це як табличка «Обережно, мокра підлога». З нею ви не перестаєте ходити по підлозі, ви просто перестаєте падати.
5. Мініприклад: Library і методи add/list/findFirst
Тепер зберімо маленький, але цілісний шматок нашого майбутнього консольного застосунку — умовно назвемо його MiniLibrary. У нас буде Book і Library, де бібліотека зберігає масив книг. Ми вже вміємо працювати з масивами й циклами, уміємо first(where:), уміємо працювати з рядками — тож нічого з майбутнього тут не потрібно.
Важливо: Library буде struct, а отже будь-які зміни списку книг — це зміна значення Library. Тому методи, які додають або видаляють книги, будуть mutating.
Додавання і виведення списку
struct Book {
var title: String
var author: String
var year: Int
func displayLine() -> String {
"\(title) — \(author) (\(year))"
}
}
struct Library {
var books: [Book] = []
mutating func add(_ book: Book) {
books.append(book)
}
func list() {
for b in books {
print(b.displayLine())
}
}
}
var lib = Library()
lib.add(Book(title: "Dune", author: "Frank Herbert", year: 1965))
lib.add(Book(title: "1984", author: "George Orwell", year: 1949))
lib.list()
// Dune — Frank Herbert (1965)
// 1984 — George Orwell (1949)
Зверніть увагу на маленьку, але важливу архітектурну перемогу: друк книги (displayLine) схований усередині Book, а логіка «перебери всі книги» — усередині Library. Зовнішній код не знає, як саме форматуються рядки книги. І це чудово: менше зв’язаності, менше копіпасту, менше шансів помилитися.
Пошук
Додамо пошук. Зробімо метод, який повертає першу книгу, знайдену за підрядком у назві. Це дуже спрощено, зате для демонстрації методів — ідеально.
struct Library {
var books: [Book] = []
mutating func add(_ book: Book) {
books.append(book)
}
func findFirst(byTitle query: String) -> Book? {
return books.first { $0.title.contains(query) }
}
}
let lib = Library(books: [
Book(title: "Dune", author: "Frank Herbert", year: 1965),
Book(title: "1984", author: "George Orwell", year: 1949)
])
if let found = lib.findFirst(byTitle: "Du") {
print(found.displayLine()) // Dune — Frank Herbert (1965)
}
Пошук не змінює стан, тому findFirst — звичайний func.
6. Типові помилки
Помилка №1: намагатися змінювати stored properties усередині звичайного func і дивуватися помилці компіляції.
Це трапляється майже з усіма, бо мозок ще думає: «Ну я ж усередині структури, чому не можна?». Але правило просте: якщо метод змінює стан, він обов’язково має бути mutating. Привчайтеся сприймати це як чесне маркування «я змінюю значення», а не як бюрократію.
Помилка №2: оголосити екземпляр через let, а потім намагатися викликати mutating‑метод.
Дуже частий сценарій: «Я ж хочу лише один раз змінити назву» — і рука тягнеться до let. Але let у Swift фіксує значення цілком: якщо ви плануєте зміни, використовуйте var. Це особливо важливо в struct, тому що мутація — це зміна значення, а let саме це забороняє. Така поведінка — очікувана частина дизайну типів-значень у Swift.
Помилка №3: писати методи, які роблять усе одразу, а потім не розуміти, що саме вони змінили.
Метод mutating func doStuff() на 80 рядків зазвичай перетворюється на чорну скриньку: він щось там робить, щось змінює, щось друкує. Через тиждень ви самі собі не поясните, чому бібліотека раптом стала порожньою. Краще тримати методи маленькими: один метод — одна зрозуміла дія (add, remove, rename, reset).
Помилка №4: дублювати логіку зовні, ігноруючи методи, які вже є в типу.
Якщо ви зробили Book.displayLine(), але продовжуєте в трьох місцях писати "\(book.title) — \(book.author)", ви самі створюєте собі розбіжність: змінили формат в одному місці — забули в іншому. Метод якраз і потрібен, щоб формат був один і жив поруч із даними.
Помилка №5: сприймати mutating як щось страшне й уникати його будь-якою ціною.
Іноді новачки починають повертати нові копії з кожного методу просто тому, що бояться слова mutating. Але у struct цілком нормальне життя з мутаціями: стандартна бібліотека повна mutating‑методів у колекцій, наприклад розворот колекції «на місці» через reverse(), і це вважається нормальним, читабельним API.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ