1. Навіщо потрібні області видимості
Коли ви пишете маленьку програму на 20 рядків, зазвичай усе лежить у main, і здається, що жодних спеціальних правил не потрібно: «я й так бачу весь код». Але щойно ви починаєте виносити логіку у функції (і це правильно), у файлі зʼявляється дедалі більше імен: printMenu, readChoice, sumExpenses, formatMoney тощо. У цей момент мозок починає працювати як компілятор: «а що з цього можна чіпати?», «а де це оголошено?», «а чому його тут не видно?».
Щоб мозок не перегрівався (а компілятор не починав влаштовувати вам Unresolved reference), у Kotlin є два механізми впорядкування:
- Модифікатори видимості (public, private) відповідають на запитання: хто може використовувати це оголошення ззовні?
- Scope (область видимості) відповідає на запитання: у якому місці коду імʼя існує й доступне?
Зовні ці слова схожі, але їхній зміст різний. Давайте чітко це розмежуємо.
Видимість: public і private
Коли ми говоримо про «видимість», то обговорюємо межі доступу. Це вже не про те, де змінну оголошено, а про те, хто має право до неї звертатися. Уявіть, що ваш файл — це маленький «магазин функцій». Деякі функції — це вітрина: користувач заходить і бачить їх. А деякі — це службові приміщення: туди ходить лише персонал, і клієнту там нічого робити (та й узагалі шкідливо, якщо він туди потрапить).
У межах цієї лекції нам потрібні лише два модифікатори:
- public — доступно «ззовні». На рівні файла (top-level) це значення за замовчуванням, тобто public зазвичай можна не писати.
- private — доступно лише всередині цього файла (якщо це top-level функція або змінна).
Це особливо корисно, коли у вас є функції-помічники: вони потрібні, щоб реалізувати сценарій, але ви не хочете, щоб хтось випадково почав викликати їх як «офіційний API».
Для Kotlin дуже типовий підхід до видимості: ховати допоміжні функції введення як private, щоб не засмічувати «публічну вітрину» файла. Такий підхід можна зустріти навіть у шаблонах, де оголошують private fun readInt() = ....
Мініприклад: публічна функція і приватний помічник
fun main() {
runApp()
}
private fun runApp() {
printHeader()
println("Застосунок запущено")
}
private fun printHeader() {
println("=== BudgetBuddy ===") // === BudgetBuddy ===
}
Тут main() за замовчуванням public (не пишемо, але маємо на увазі), а runApp() і printHeader() — наші внутрішні деталі. Якби файл був частиною великого проєкту, ми б не хотіли, щоб хтось «ззовні» викликав printHeader() напряму.
2. Область видимості: де імʼя існує
Тепер — про область видимості. Це вже не про «можна чи ні», а про «чи видно взагалі». Навіть якщо щось public, воно не стає доступним «усюди на планеті»: імʼя все одно існує в конкретній області коду.
Найчастіша думка новачка звучить так: «Я ж оголосив змінну — чому я не можу використати її нижче?» Відповідь проста: тому що ви оголосили її усередині блоку, а блок закінчився — змінна «померла» (спокійно, це нормальна й навіть корисна смерть).
У Kotlin області видимості найчастіше задаються фігурними дужками { ... }. І це не декоративні дужки, а прямі межі «життя» імен.
Приклад: змінна живе лише всередині if
fun main() {
val x = 10
if (x > 0) {
val msg = "x додатний"
println(msg) // x додатний
}
println(msg) // Помилка компіляції: msg тут уже не існує
}
msg оголошено всередині блоку if. Після закривальної } імʼя msg зникає: ви не можете до нього звернутися, навіть якщо вам дуже хочеться.
Той самий принцип для while і for
fun main() {
var sum = 0
for (i in 1..3) {
val add = i * 10
sum += add
}
println(sum) // 60
println(add) // Помилка: add видима лише всередині тіла циклу
}
Змінна add існує лише всередині тіла циклу. Це захищає вас від випадкового використання «тимчасових» змінних там, де вони вже не мають сенсу.
3. Корисні нюанси
private/public і scope — не одне й те саме
Тут легко заплутатися, тож закріпімо це на зрозумілій аналогії. Уявіть, що у вас є ключі й стіни.
Scope — це стіни кімнати. Якщо ви вийшли з кімнати, ви не бачите речей, які залишилися всередині.
Visibility (private/public) — це ключі від дверей. Навіть якщо річ лежить у кімнаті, ви можете не мати права туди зайти.
Тобто:
- імʼя може бути в області видимості (воно «тут живе»), але водночас бути недоступним через private (наприклад, з іншого файла);
- імʼя може бути public, але все одно не існувати в поточному місці коду (якщо ви вийшли з блоку).
Щоб не тримати це в голові суцільною кашею, корисно подивитися на таблицю.
| Що ми обговорюємо | Запитання | Приклад | Хто «керує» |
|---|---|---|---|
| Область видимості (scope) | «Де імʼя існує?» | |
Фігурні дужки, структура коду |
| Видимість (visibility) | «Хто може використовувати?» | |
Модифікатори private/public |
Локальні функції й область видимості
У попередній лекції ви познайомилися з локальними функціями: це функції, оголошені всередині іншої функції. Зараз саме час подивитися на них крізь призму області видимості: локальна функція видна лише всередині зовнішньої функції. За її межами вона не існує.
Це часто дуже зручно: ви не захаращуєте файл десятком «helperSomething», якщо вони потрібні лише в одному сценарії.
Приклад: локальна функція для форматування грошей
fun main() {
printReceipt(totalCents = 2599)
}
fun printReceipt(totalCents: Int) {
fun formatMoney(cents: Int): String {
val dollars = cents / 100
val rest = cents % 100
return "$dollars.${rest.toString().padStart(2, '0')}"
}
println("Разом: ${formatMoney(totalCents)}") // Разом: 25.99
}
formatMoney — це «внутрішній співробітник». Він існує лише всередині printReceipt. Ззовні його не викликати (і це плюс: менше шансів випадково щось зламати).
6. Перекриття імен: shadowing
Якщо ви любите шукати в усьому підступ, то вже мали б поставити запитання: а що буде, якщо в області видимості є кілька змінних з однаковими іменами?
Ви можете оголосити змінну з таким самим іменем, як інша змінна рівнем вище. Це називається перекриття імені/затінення (shadowing). Компілятор часто це дозволяє, але читабельність майже завжди страждає.
Головна небезпека не в тому, що «програма не працює», а в тому, що ви потім читаєте код і думаєте: «а x — це який x?»
Приклад: однакові імена в різних блоках
fun main() {
val count = 10
run {
val count = 3
println(count) // 3
}
println(count) // 10
}
Усередині блоку run { ... } count — це інший count. Зовнішній нікуди не подівся, але його «сховано» всередині блоку. Технічно це інколи може бути потрібно, але в навчальних і прикладних проєктах найчастіше це просто випадкова плутанина.
7. Проєкт BudgetBuddy і порядок у видимості
Зараз ми акуратно застосуємо private/public і scope на практиці. Нехай у нас буде простий консольний застосунок BudgetBuddy: ми зберігаємо витрати в масиві, додаємо нові суми й друкуємо статистику. Зробимо це в одному файлі, щоб поки не заходити в теми про структуру проєкту й пакети.
Одразу домовимося про стиль: «ззовні» нам потрібна лише точка входу main(). Усе інше — деталі реалізації, отже, логічно зробити їх private, щоб файл виглядав як акуратний «міні-модуль».
Крок 1: вітрина файла — лише main()
fun main() {
runBudgetBuddy()
}
private fun runBudgetBuddy() {
println("=== BudgetBuddy ===") // === BudgetBuddy ===
}
Уже на цьому кроці файл читається приємно: main запускає сценарій, а сам сценарій заховано всередині.
Крок 2: додамо стан і покажемо scope змінних
Зберігати витрати будемо в IntArray, де число — це сума в центах (щоб не звʼязуватися з Double і питаннями точності).
fun main() {
runBudgetBuddy()
}
private fun runBudgetBuddy() {
val expenses = IntArray(5)
var size = 0
println("Додамо тестові витрати...")
expenses[0] = 199
expenses[1] = 550
size = 2
println("Записів: $size") // Записів: 2
}
Тут expenses і size живуть лише всередині runBudgetBuddy. Це нормально: це внутрішній стан сценарію.
Крок 3: розділяємо на функції й ховаємо помічників
Додамо меню й вибір команди. Зверніть увагу: ми не робимо «ідеальний продукт», ми тренуємо видимість і області імен.
fun main() {
runBudgetBuddy()
}
private fun runBudgetBuddy() {
println("=== BudgetBuddy ===") // === BudgetBuddy ===
while (true) {
printMenu()
val cmd = readln().trim()
if (cmd == "exit") return
println("Команда: $cmd") // Команда: ...
}
}
private fun printMenu() {
println()
println("Введіть команду: add / list / exit")
}
printMenu() — звичайний помічник. Він не потрібен нікому поза файлом, тому private підходить ідеально.
До речі, звичка позначати такі функції як private корисна навіть в «однофайлових» задачах: так ви не даєте файлу перетворитися на звалище «публічних випадкових функцій». Підхід із private допоміжними функціями часто використовують у шаблонах для введення (наприклад, private fun readInt()), щоб не плодити публічні оголошення.
Крок 4: блоки й scope усередині if/else if
І тут чудово видно, як працюють блоки й scope.
fun main() {
runBudgetBuddy()
}
private fun runBudgetBuddy() {
val expenses = IntArray(10)
var size = 0
while (true) {
printMenu()
val cmd = readln().trim()
if (cmd == "add") {
val added = addExpense(expenses, size)
size += added
} else if (cmd == "list") {
printExpenses(expenses, size)
} else if (cmd == "exit") {
return
} else {
println("Невідома команда: $cmd")
}
}
}
private fun printMenu() {
println()
println("Введіть команду: add / list / exit")
}
Дивіться уважно: змінна cmd живе всередині тіла циклу while. А змінна added — лише всередині блоку if (cmd == "add") { ... }. І це добре: ми не зможемо випадково використати added у гілці "list".
Тепер додамо самі функції (коротко й зрозуміло):
private fun addExpense(expenses: IntArray, size: Int): Int {
if (size >= expenses.size) {
println("Масив заповнено, більше додати не можна")
return 0
}
print("Введіть суму в центах: ")
val cents = readln().trim().toInt()
expenses[size] = cents
return 1
}
private fun printExpenses(expenses: IntArray, size: Int) {
println("Витрати:")
for (i in 0 until size) {
println("- ${expenses[i]} центів")
}
}
Зверніть увагу на маленьку, але важливу річ: size у addExpense — це параметр. Він лише для читання: перепризначити його не можна. А от expenses[size] = cents — можна, тому що це зміна масиву, а не перепризначення параметра.
Крок 5: ховаємо форматування й уникаємо перекриття імен
Зробимо форматування грошей як приватну функцію. Можна було б зробити її локальною, але вона знадобиться в кількох місцях. Отже, нехай буде private на рівні файла.
private fun formatMoney(cents: Int): String {
val dollars = cents / 100
val rest = cents % 100
return "$dollars.${rest.toString().padStart(2, '0')}"
}
private fun printExpenses(expenses: IntArray, size: Int) {
println("Витрати:")
for (i in 0 until size) {
println("- ${formatMoney(expenses[i])}")
}
}
Тут важливо, що rest — імʼя всередині formatMoney. Воно не «протікає» назовні. І це чудово: що менше імен живе довго, то менше шансів випадково «наступити» на них в іншому місці.
Мінісхема: як читати файл із private-помічниками
Коли ви вибудовуєте файл так, що назовні «стирчить» лише main(), а решта — деталі, читання стає лінійним: «запуск → сценарій → помічники».
flowchart TD
A["public main()"] --> B["private runBudgetBuddy()"]
B --> C["private printMenu()"]
B --> D["private addExpense(...)"]
B --> E["private printExpenses(...)"]
E --> F["private formatMoney(...)"]
Такий файл простіше підтримувати: ви одразу бачите, яка функція «головна», а які — обслуговувальні.
8. Типові помилки
Помилка № 1: плутати private і область видимості, очікуючи, що private «ховає змінну від сусіднього блоку».
Іноді здається, що якщо зробити щось private, то воно зникне «десь поруч». Але private/public — це про доступ між файлами (і іншими зовнішніми частинами проєкту), а не про блоки { ... }. Від блоку вас захищає саме scope: змінна всередині if не видна ззовні не тому, що вона «private», а тому що блок закінчився.
Помилка № 2: оголошувати змінну в блоці, а потім намагатися використати її після блоку.
Класика жанру: ви зчитали введення в if, створили val value, а нижче хочете його надрукувати. Компілятор справедливо каже Unresolved reference. Рішення зазвичай просте: оголосити змінну до блоку, а всередині — присвоїти значення. Тоді імʼя житиме довше, і ви свідомо оберете строк його життя.
Помилка № 3: перекриття імен (shadowing) «заради зручності», яке потім перетворюється на плутанину.
Новачки іноді повторно оголошують val x усередині блоку, бо «мені так простіше». Так, простіше зараз — але за 15 хв ви самі зʼясовуватимете, який x використовується в конкретному рядку. Краще давати різні імена: userChoice, menuChoice, newChoice — нехай довші, зате мозок не працюватиме як детектив.
Помилка № 4: робити все public просто тому, що «так працює».
Якщо ви лишаєте всі top-level функції публічними, файл перетворюється на набір випадкових «зовнішніх обіцянок». Навіть якщо зараз проєкт маленький, звичка ховати помічників як private допомагає дисциплінувати дизайн: є вхідний сценарій і є внутрішня кухня. У результаті код легше читати й складніше випадково використати «не ту» функцію.
Помилка № 5: створювати надто багато змінних із великим scope «про всяк випадок».
Іноді, щоб перемогти помилки видимості, новачок оголошує все на початку функції: var a, var b, var c — і потім присвоює їм значення в процесі. Це працює, але робить код каламутним: незрозуміло, коли змінна справді потрібна і в якому стані вона перебуває. Набагато приємніше тримати змінні якомога ближче до місця використання й дозволяти scope обмежувати їхнє життя.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ