JavaRush /Курси /Kotlin SELF /Видимість і область видимості: private/public

Видимість і область видимості: private/public

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

1. Навіщо потрібні області видимості

Коли ви пишете маленьку програму на 20 рядків, зазвичай усе лежить у main, і здається, що жодних спеціальних правил не потрібно: «я й так бачу весь код». Але щойно ви починаєте виносити логіку у функції (і це правильно), у файлі зʼявляється дедалі більше імен: printMenu, readChoice, sumExpenses, formatMoney тощо. У цей момент мозок починає працювати як компілятор: «а що з цього можна чіпати?», «а де це оголошено?», «а чому його тут не видно?».

Щоб мозок не перегрівався (а компілятор не починав влаштовувати вам Unresolved reference), у Kotlin є два механізми впорядкування:

  1. Модифікатори видимості (public, private) відповідають на запитання: хто може використовувати це оголошення ззовні?
  2. 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) «Де імʼя існує?»
{
    val t = 1
}
Фігурні дужки, структура коду
Видимість (visibility) «Хто може використовувати?»
private fun helper()
Модифікатори 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 обмежувати їхнє життя.

1
Опитування
Функції та їхня структура, рівень 8, лекція 5
Недоступний
Функції та їхня структура
Функції та їхня структура
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ