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

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

Kotlin SELF
8 уровень , 5 лекция
Открыта

1. Зачем нужны области видимости

Когда вы пишете маленькую программу из 20 строк, обычно всё лежит в main, и кажется, что никаких специальных правил не нужно: «я и так вижу весь код». Но как только вы начинаете выносить логику в функции (и это правильно), в файле появляется всё больше имён: printMenu, readChoice, sumExpenses, formatMoney и так далее. В этот момент мозг начинает работать как компилятор: «а что из этого можно трогать? а где это объявлено? а почему оно тут не видно?».

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

  1. Visibility modifiers (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]} cents")
    }
}

Обратите внимание на маленькую, но важную вещь: size в addExpense — это параметр. Он “read-only”: переназначить его нельзя. А вот 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
Задача
Kotlin SELF, 8 уровень, 5 лекция
Недоступна
Скрытая кухня
Скрытая кухня
1
Задача
Kotlin SELF, 8 уровень, 5 лекция
Недоступна
Область видимости
Область видимости
1
Задача
Kotlin SELF, 8 уровень, 5 лекция
Недоступна
Печать чека
Печать чека
1
Задача
Kotlin SELF, 8 уровень, 5 лекция
Недоступна
Купон без теней
Купон без теней
1
Опрос
Функции и их структура, 8 уровень, 5 лекция
Недоступен
Функции и их структура
Функции и их структура
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ