JavaRush /Курси /Kotlin SELF /Синтаксис класу: primary constructor, властивості в загол...

Синтаксис класу: primary constructor, властивості в заголовку, значення за замовчуванням

Kotlin SELF
Рівень 30 , Лекція 1
Відкрита

1. Primary constructor і властивості

Коли ви вперше бачите клас у Kotlin, може здатися, ніби це якийсь «особливий різновид функції» з фігурними дужками. І це майже правда: створення об’єкта справді схоже на виклик функції. Primary constructor — це ті самі «вхідні двері», через які ми передаємо початкові значення в новий об’єкт. Його записують прямо в заголовку класу — одразу після імені.

У Kotlin primary constructor оголошують у заголовку класу: після імені, у круглих дужках. Якщо немає анотацій і модифікаторів видимості, ключове слово constructor можна не писати.

Подивімося на найпростіший варіант — із явним constructor (так легше побачити, де він починається і де закінчується):


class Expense constructor(title: String, amount: Int)

fun main() {
    val e = Expense("Coffee", 300)
    println(e) // Expense@<якийсь_хеш>
}

Так, цей код скомпілюється, і об’єкт створиться. Але є важлива деталь: title і amount зараз не стали властивостями. Це лише параметри, що прийшли «на вхід». Далі вони нікуди не записалися, бо ми не сказали Kotlin, що хочемо їх зберігати.

Щоб проблема була наочнішою, уявімо, що ми намагаємося зробити так:

class Expense constructor(title: String, amount: Int)

fun main() {
    val e = Expense("Coffee", 300)
    // println(e.title) // так не можна: title не є властивістю
}

Компілятор матиме рацію: в об’єкта немає властивості title, бо ми її не оголосили.

Параметр конструктора і властивість

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

У Kotlin primary constructor може оголошувати параметри як властивості: val створює властивість лише для читання, а var — змінювану. Такі властивості зберігаються в екземплярі й доступні ззовні через крапку.

Зробімо нормальну версію Expense:

class Expense(val title: String, val amount: Int)

fun main() {
    val e = Expense("Coffee", 300)

    println(e.title)  // Coffee
    println(e.amount) // 300
}

Тепер title і amountвластивості. Вони «живуть» усередині об’єкта, тож ми можемо їх читати.

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

Запис у конструкторі Це властивість об’єкта? Можна звернутися як obj.x? Де можна використовувати
name: String
ні ні лише всередині тіла класу (якщо воно взагалі є)
val name: String
так (лише для читання) так і всередині класу, і ззовні
var name: String
так (змінювана) так і всередині класу, і ззовні

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

class User(nameInput: String) {
    val name: String = nameInput.trim()
}

fun main() {
    val u = User("   Ann   ")
    println(u.name) // Ann

    // println(u.nameInput) // не можна: це не властивість
}

Корисний спосіб мислити так: параметр — це те, що прийшло під час створення, а властивість — те, що ви вирішили залишити всередині об’єкта.

2. Створення об’єктів без new

Коли ми створюємо об’єкт, то пишемо ім’я класу й круглі дужки — ніби викликаємо функцію. І це не випадково: конструктор справді викликається як «функція створення екземпляра». У Kotlin немає ключового слова new — об’єкт створюється просто через ClassName(...).

Зробімо приклад у стилі нашого застосунку (консольного трекера витрат). Поки що просто створімо кілька витрат і виведімо їх:

class Expense(val title: String, val amount: Int)

fun main() {
    val coffee = Expense("Coffee", 300)
    val taxi = Expense("Taxi", 1200)

    println("${coffee.title}: ${coffee.amount}") // Coffee: 300
    println("${taxi.title}: ${taxi.amount}")     // Taxi: 1200
}

Зверніть увагу на тонкість у моделі «посилань на об’єкт». Навіть якщо змінну оголошено як val, це означає «посилання не можна перепризначити». Але це не означає, що «об’єкт магічно заморожено». У нашому прикладі властивості теж val, тож об’єкт виходить «стабільним».

Ось мінідемонстрація «двох імен одного об’єкта», щоб у голові трималася ця модель:

class Expense(val title: String, val amount: Int)

fun main() {
    val a = Expense("Coffee", 300)
    val b = a

    println(a === b) // true
}

a === b — істинно, бо це одне й те саме посилання. Це знання знадобиться, коли з’являться var-властивості. Але сьогодні наша мета — саме синтаксис конструктора.

3. Значення за замовчуванням

Щойно в класу з’являється більше ніж два поля, виникає новий біль: «Невже щоразу треба передавати все, навіть якщо частина значень зазвичай однакова?» Kotlin розв’язує це так само, як і для функцій: значеннями за замовчуванням.

У primary constructor можна задавати значення за замовчуванням прямо поруч із параметрами або властивостями. Якщо значення не передано під час створення, буде використано значення за замовчуванням.

Додаймо в наш Expense категорію й зробімо для неї значення за замовчуванням (звичайним рядком, без ускладнень):

class Expense(
    val title: String,
    val amount: Int = 0,
    val category: String = "OTHER"
)

fun main() {
    val e1 = Expense("Coffee")
    val e2 = Expense("Taxi", 1200)
    val e3 = Expense("Lunch", 900, "FOOD")

    println("${e1.title}, ${e1.amount}, ${e1.category}") // Coffee, 0, OTHER
    println("${e2.title}, ${e2.amount}, ${e2.category}") // Taxi, 1200, OTHER
    println("${e3.title}, ${e3.amount}, ${e3.category}") // Lunch, 900, FOOD
}

Сенс значень за замовчуванням тут не в тому, щоб «замаскувати проблему». Сенс у тому, щоб зробити створення об’єкта зручним там, де значення справді часто однакові. І водночас — не розмазувати магічні числа та рядки по всьому коду.

Ще один практичний момент: значення за замовчуванням допомагають створювати «мінімально валідний» об’єкт без зайвого шуму. Наприклад, якщо в нашій команді add користувач може не вказувати категорію, ми спокійно створюємо об’єкт із "OTHER".

4. Іменовані аргументи

Коли в конструктора кілька параметрів одного типу (наприклад, два рядки поспіль), дуже легко переплутати їх місцями. У людини в голові «ну це ж очевидно», а для комп’ютера — «ви передали рядок у рядок, усе законно». Щоб зменшити такі помилки, Kotlin дозволяє викликати конструктор з іменованими аргументами — так само, як і функції.

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

class Expense(
    val title: String,
    val amount: Int = 0,
    val category: String = "OTHER"
)

fun main() {
    val e = Expense(
        title = "Books",
        amount = 1500,
        category = "EDUCATION"
    )

    println("${e.title}: ${e.amount} (${e.category})") // Books: 1500 (EDUCATION)
}

Тепер виклик складно «випадково зламати», бо сенс параметрів написано прямо в коді.

Іменовані аргументи чудово поєднуються зі значеннями за замовчуванням: можна вказати лише те, що потрібно, і не думати про «дірки» всередині списку аргументів:

class Expense(
    val title: String,
    val amount: Int = 0,
    val category: String = "OTHER"
)

fun main() {
    val e = Expense(title = "Coffee", category = "FOOD")
    println("${e.title}, ${e.amount}, ${e.category}") // Coffee, 0, FOOD
}

Зверніть увагу: ми пропустили amount, і це нормально, бо для нього є значення за замовчуванням.

5. Читабельний заголовок класу

Перші два поля зазвичай уміщаються в один рядок — і все виглядає добре. Але життя швидко приносить третій параметр, потім четвертий… і раптом заголовок класу стає ширшим за екран, а читати його можна лише з горизонтальним прокручуванням. Тож корисно заздалегідь домовитися зі своїм майбутнім «я», як саме ви форматуватимете конструктор.

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

class Expense(
    val title: String,
    val amount: Int = 0,
    val category: String = "OTHER"
)

Ще один важливий момент: якщо параметр не повинен бути доступним ззовні, не робіть його властивістю «за звичкою». Іноді хочеться поставити val всюди — «про всяк випадок». Але так ви без потреби розширюєте API класу. Намагайтеся, щоб у заголовку залишалися лише справді потрібні поля моделі.

Невелика схема того, що відбувається під час створення об’єкта (дуже спрощено, але корисно, щоб укласти в голові):

flowchart TD
    A["Expense(...)"] --> B["Primary constructor отримує аргументи"]
    B --> C["val/var параметри стають властивостями"]
    C --> D["Обʼєкт створено, доступ через крапку: e.title, e.amount"]

Ця схема — про ту частину, яку ми вивчаємо сьогодні: «передали → зберегли у властивостях → читаємо через крапку».

6. Міграція від Pair до класу Expense

До класів ми часто робили так: одна витрата — це Pair<String, Int>, де first — назва, а second — сума. Це працювало, але читалося як «записка на холодильнику»: сенс є, та його потрібно тримати в пам’яті. Тепер ми замінимо це на Expense(title, amount, category).

Покажу зовсім маленький фрагмент «як було» (умовно, у нашому CLI-трекері витрат):

fun main() {
    val expenses = mutableListOf<Pair<String, Int>>()
    expenses.add("Coffee" to 300)

    val (title, amount) = expenses[0]
    println("$title: $amount") // Coffee: 300
}

Тепер «як буде». Створімо клас і список об’єктів:

class Expense(val title: String, val amount: Int)

fun main() {
    val expenses = mutableListOf<Expense>()
    expenses.add(Expense("Coffee", 300))

    val e = expenses[0]
    println("${e.title}: ${e.amount}") // Coffee: 300
}

З погляду операцій зі списком нічого магічного не змінилося: MutableList як був, так і залишився. Але читати стало по-людськи: e.title замість pair.first. У цьому й полягає той самий «прибуток» від моделі предметної області.

Тепер додаймо значення за замовчуванням для категорії, щоб наша команда add могла бути простішою:

class Expense(
    val title: String,
    val amount: Int,
    val category: String = "OTHER"
)

fun main() {
    val expenses = mutableListOf<Expense>()

    expenses.add(Expense("Coffee", 300, category = "FOOD"))
    expenses.add(Expense("Taxi", 1200)) // category = OTHER

    for (e in expenses) {
        println("${e.title}: ${e.amount} (${e.category})")
        // Coffee: 300 (FOOD)
        // Taxi: 1200 (OTHER)
    }
}

Це вже виглядає як заготовка для нормального застосунку: ми можемо зберігати дані структуровано, а введення й команди в main просто створюватимуть об’єкти.

7. Типові помилки під час оголошення класів і primary constructor

Помилка № 1: забули val/var і очікували, що параметр стане властивістю.
Дуже поширена ситуація: написали class Expense(title: String, amount: Int) і потім здивувалися, чому e.title не існує. Ліки одні: якщо поле має бути доступним через крапку й зберігатися в об’єкті — оголошуйте його як val title: String або var title: String у заголовку. Якщо поле потрібне лише як «вхід», залишайте його без val/var. Але тоді всередині класу явно збережіть його у властивість.

Помилка № 2: зробили все var, бо «раптом знадобиться».
Це не синтаксична помилка, але логічна міна сповільненої дії. Що більше var, то більше місць, де об’єкт може змінитися несподівано, і тим складніше налагоджувати «чому сума стала 0». На старті краще обирати val як варіант за замовчуванням і додавати var лише тоді, коли ви можете чесно відповісти: «Так, за змістом це поле має змінюватися після створення об’єкта».

Помилка № 3: переплутали порядок аргументів у конструкторі.
Коли в конструкторі кілька параметрів, особливо однакових типів, позиційні аргументи легко переплутати. Компілятор не врятує: String на місці String виглядає законно. Тому там, де сенс важливіший за швидкість набору, використовуйте іменовані аргументи: Expense(title = "...", amount = ...). Це помітно знижує кількість тихих помилок.

Помилка № 4: зловживали значеннями за замовчуванням і «сховали сенс».
Значення за замовчуванням зручні, але ними можна переборщити: коли в класу 8 параметрів і 7 із них мають значення за замовчуванням, об’єкт починає створюватися «якось сам», і стає незрозуміло, що насправді важливо. Хороша перевірка: якщо параметр важливий завжди (наприклад, сума витрати), імовірно, він не повинен мати значення за замовчуванням. Тоді помилка ловитиметься одразу під час створення.

Помилка № 5: надто довгий заголовок класу в один рядок.
Синтаксис Kotlin дозволяє, але очі — не зобовʼязані. Якщо конструктор не вміщується в один рядок без горизонтального прокручування, переносьте параметри на окремі рядки. Це не «косметика»: читабельність напряму впливає на кількість багів, особливо коли ви лише вчитеся й тримаєте в голові одразу багато нового.

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