JavaRush /Курси /Swift SELF /Синтаксис класу — властивості та методи

Синтаксис класу — властивості та методи

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

1. Вступ

Коли ви вперше бачите class, виникає цілком чесне запитання: «Ми ж щойно навчилися жити зі struct, навіщо ще одна сутність?» Це нормально. class у Swift — не «покращений struct», а інший інструмент. Він потрібен тоді, коли ви моделюєте обʼєкт з ідентичністю, який той самий для різних частин програми, і коли зміни мають бути видимими всім, хто на нього дивиться.

Уявіть бібліотекаря — це ваш обʼєкт — і картку книги, тобто значення. Картка книги — це struct: скопіювали, переписали, поклали поруч — і все гаразд. А ось бібліотекар — це радше class: одна й та сама людина. Якщо він поставив штамп, це вже зміна для всіх, а не ситуація, коли в одній копії бібліотекаря штамп є, а в іншій — ні.

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

2. Базовий синтаксис class: оголошення і створення екземпляра

Синтаксис оголошення класу майже такий самий, як у struct: ключове слово, імʼя типу, фігурні дужки. Часто до оголошення додають модифікатори на кшталт final, але в цій лекції нам достатньо базової форми.

Ось мінімальний клас — зовсім «порожній», але вже існуючий:

import Foundation

class Library {
}

let lib = Library()
print(lib) // Бібліотека

Сенс цього коду поки що простий: ми навчилися створювати екземпляр класу через Library(). Важливо запамʼятати: дужки означають «створити обʼєкт». Навіть якщо всередині немає параметрів, обʼєкт усе одно створюється.

Тепер додамо властивість і метод — і отримаємо вже корисний обʼєкт.

import Foundation

class Counter {
    var value: Int = 0

    func increment() {
        value += 1
    }
}

let c = Counter()
c.increment()
print(c.value) // 1

Зверніть увагу на два моменти. По-перше, value — це stored property, тобто властивість, що зберігає дані. По-друге, increment() — це метод екземпляра (instance method), який викликається як c.increment().

3. Властивості класу: var, let, значення за замовчуванням

Властивості класу — це його стан. На практиці саме довкола стану й виникає найбільше пригод: де змінюється значення, хто бачить зміну, чому let не завжди означає «незмінність» (спойлер: let може фіксувати посилання, а не сам обʼєкт).

Почнімо з найпоширенішого: var-властивостей із типовим значенням. Це зручно, бо Swift може автоматично згенерувати ініціалізатор init() без параметрів, а ви просто створюєте обʼєкт через TypeName().

import Foundation

class UserSession {
    var username: String = "Анонім"
    var isLoggedIn: Bool = false
}

let s = UserSession()
print(s.username)    // Анонім
print(s.isLoggedIn)  // false

let-властивості всередині класу

let усередині класу означає: після створення обʼєкта це поле не можна переназначити. Але щоб код компілювався, така властивість має отримати значення під час створення обʼєкта — або одразу, як значення за замовчуванням, або через init. Детальні правила ініціалізації ми розберемо окремо, а зараз нам достатньо самої ідеї: let — це «присвоїли один раз, далі незмінно».

import Foundation

class BookRecord {
    let id: Int = 1
    var title: String = "Невідомо"
}

let r = BookRecord()
print(r.id)     // 1
print(r.title)  // Невідомо

Обчислювані властивості в класі

Computed property не зберігає значення — вона обчислює його щоразу. Тому ініціалізувати її не потрібно: памʼять під дані не виділяється, є лише формула.

import Foundation

class Rectangle {
    var width: Double = 0
    var height: Double = 0

    var area: Double { width * height }
}

let rect = Rectangle()
rect.width = 2
rect.height = 3
print(rect.area) // 6.0

4. Методи класу: поведінка без mutating

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

Створімо обʼєкт «Нотатка» і додамо метод, який змінює стан.

import Foundation

class Note {
    var text: String = ""

    func append(_ suffix: String) {
        text += suffix
    }
}

let n = Note()
n.append("Привіт")
n.append(", Swift!")
print(n.text) // Привіт, Swift!

Зверніть увагу: метод виглядає як звичайна функція всередині типу. І читається це теж просто: «у нотатки викликати append».

self у методах

self — це «поточний екземпляр». Найчастіше Swift дозволяє не писати його, але іноді він допомагає зробити код зрозумілішим, особливо коли параметр методу називається так само, як і властивість.

import Foundation

class Tag {
    var name: String = ""

    func rename(to name: String) {
        self.name = name
    }
}

let t = Tag()
t.rename(to: "Swift")
print(t.name) // Swift

Тут self.name — це властивість обʼєкта, а name без self — параметр методу. Якщо написати name = name, вийде дуже філософський код без практичної користі.

5. Посилальність та ідентичність: що відрізняє class від struct

А тепер — ключовий момент усієї лекції. Якщо спростити до однієї фрази, то struct частіше відповідає на запитання «які дані?», а class — «який обʼєкт?». В одному випадку норма — копіювання, в іншому — спільний доступ до одного екземпляра.

Аліасинг: дві змінні — один обʼєкт

Створімо клас Box, покладемо всередину число і подивімося, що станеться під час присвоєння.

import Foundation

class Box {
    var x: Int = 0
}

let a = Box()
a.x = 10

let b = a
b.x = 20

print(a.x) // 20
print(b.x) // 20

Це і є аліасинг: b = a не створює нову коробку, а просто дає другу мітку для тієї самої коробки.

Для порівняння: зі struct ви б отримали копію, і зміни не поширилися б назад.

import Foundation

struct BoxValue {
    var x: Int = 0
}

var a = BoxValue()
a.x = 10

var b = a
b.x = 20

print(a.x) // 10
print(b.x) // 20

Таблиця struct vs class

Щоб у голові не було туману, корисно один раз побачити це поруч:

Тема
struct
class
Семантика тип-значення (значення копіюється) тип-посилання (посилання на обʼєкт)
Присвоєння b = a копія даних копія посилання (аліас)
Зміна в методах часто потрібен mutating mutating не потрібен
Ідентичність «той самий набір даних» «той самий екземпляр» (===)

let з класом: посилання незмінне, але обʼєкт може змінюватися

Це місце найчастіше викликає подив. У Swift запис let obj = SomeClass() означає: змінну obj не можна переназначити на інший обʼєкт. Але це не означає, що сам обʼєкт заморожено.

import Foundation

class Profile {
    var name: String = "Анонім"
}

let p = Profile()
p.name = "Alex"
print(p.name) // Alex

Чому так? Бо незмінним є посилання, а не вміст обʼєкта. Усередині обʼєкта namevar, отже його можна змінювати.

Якби ви захотіли зробити обʼєкт майже незмінним, ви б інакше спроєктували його API: більше let-властивостей, менше var, а зміни — через створення нового обʼєкта. Але це вже питання дизайну типів, а не синтаксису.

Ідентичність обʼєктів: === і запитання «це той самий екземпляр?»

Оскільки у класів є аліасинг, виникає інше завдання: іноді вам важливо зрозуміти, чи дві змінні вказують на один і той самий обʼєкт, чи на два різні обʼєкти з однаковими даними. Для цього в Swift є оператори === і !==.

import Foundation

class Token {
    var value: Int = 0
}

let first = Token()
let second = Token()
let alias = first

print(first === alias)  // true
print(first === second) // false

Це не про рівність значень, а про «той самий екземпляр».

6. Приклад: «каталог» як class, «книга» як struct

Зараз ми зберемо невеликий, але цілісний приклад, який добре показує, навіщо class взагалі може бути корисним. Уявімо найпростіший консольний застосунок «міні-бібліотека»: книга — це дані (value), а каталог — один спільний обʼєкт, який змінюється командами.

Почнімо з моделі книги. Це хороший кандидат на struct, тому що «книга» як запис — це набір значень.

import Foundation

struct Book {
    let id: Int
    var title: String
}

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

import Foundation

class LibraryCatalog {
    private(set) var books: [Book] = []

    func add(_ book: Book) {
        books.append(book)
    }
}

Тут є цікава конструкція private(set), але ми не занурюємося в деталі: просто сприймайте її так — ззовні books можна читати, але змінювати напряму не можна; лише через методи. Ми робимо це, щоб зміни були контрольованими, а не так, що хто завгодно і де завгодно додає що завгодно.

Тепер — демонстрація аліасингу каталогу:

import Foundation

let catalog = LibraryCatalog()
catalog.add(Book(id: 1, title: "Основи Swift"))

let alias = catalog
alias.add(Book(id: 2, title: "Класи у Swift"))

print(catalog.books.count) // 2
print(alias.books.count)   // 2

Обидві змінні дивляться на один каталог. Це саме той ефект, який був би незручним (або занадто дорогим) зі struct, бо каталог — це змінювана колекція, яку не хочеться щоразу копіювати цілком.

Додамо метод пошуку

Створімо метод, який шукає книгу за id. Він повертає Book?, бо потрібної книги може не бути.

import Foundation

extension LibraryCatalog {
    func findBook(byID id: Int) -> Book? {
        books.first { $0.id == id }
    }
}

let c = LibraryCatalog()
c.add(Book(id: 10, title: "Алгоритми"))

print(c.findBook(byID: 10)?.title ?? "не знайдено") // Алгоритми
print(c.findBook(byID: 99)?.title ?? "не знайдено") // не знайдено

Ми використовуємо те, що ви вже проходили: Optional, оператор ?? і роботу з масивами.

Міні-точка входу як CLI-демо

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

import Foundation

let catalog = LibraryCatalog()
catalog.add(Book(id: 1, title: "Вступ до Swift 6.2"))
catalog.add(Book(id: 2, title: "Struct vs Class"))

for book in catalog.books {
    print("#\(book.id): \(book.title)")
}
// #1: Вступ до Swift 6.2
// #2: Struct vs Class

Це проста сцена, але вона дуже чесно показує ролі: Book — значення, LibraryCatalog — спільний обʼєкт зі станом.

7. Типові помилки під час роботи з class

Помилка №1: очікувати, що b = a створює копію обʼєкта.
Після тривалого життя зі struct мозок звикає до «копій», і рука автоматично думає, що присвоєння створює незалежний екземпляр. Для class це не так: ви копіюєте посилання. Якщо в іншому місці застосунку зʼявилися дивні зміни, насамперед перевірте, чи не виник аліас.

Помилка №2: плутати рівність даних та ідентичність.
Два різні обʼєкти можуть мати однакові поля, але бути різними екземплярами. І навпаки: дві змінні можуть посилатися на один обʼєкт, і тоді === буде true, навіть якщо ви ще не змінювали властивості. Якщо все змішалося, запамʼятайте просте правило: == зазвичай про дані, === — про «той самий екземпляр».

Помилка №3: вважати, що let obj = SomeClass() робить обʼєкт незмінним.
let фіксує змінну-посилання, але не забороняє змінювати var-властивості обʼєкта. Через це іноді й виникає неочікуваний ефект: «чому в мене константа, а все змінюється?» Бо константа — це посилання, а не внутрішній стан обʼєкта. Хочете незмінність — проєктуйте API з меншою кількістю var.

Помилка №4: перетворювати клас на «мішок публічних полів», які змінюються звідки завгодно.
Клас легко зробити надто «всемогутнім»: багато var, доступних усім, і жодної логіки контролю. Тоді будь-яке місце коду може зламати інваріанти обʼєкта. Навіть без вивчення просунутих тем корисно триматися простого правила: якщо зміна має бути осмисленою дією, оформлюйте її методом (add, remove, rename), а не прямим доступом до масиву чи рядка.

Помилка №5: занадто рано обирати class «бо так звичніше після інших мов».
Новачки з Java/C# часто автоматично обирають class. У Swift це не завжди найкращий старт: struct простіший для розуміння й безпечніший за замовчуванням, бо дає менше неочікуваних побічних ефектів через аліасинг. Корисна звичка: починати зі struct і переходити на class, коли вам справді потрібні спільна ідентичність і розділюваний стан.

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