1. Зачем нужны области видимости
Когда вы пишете маленькую программу из 20 строк, обычно всё лежит в main, и кажется, что никаких специальных правил не нужно: «я и так вижу весь код». Но как только вы начинаете выносить логику в функции (и это правильно), в файле появляется всё больше имён: printMenu, readChoice, sumExpenses, formatMoney и так далее. В этот момент мозг начинает работать как компилятор: «а что из этого можно трогать? а где это объявлено? а почему оно тут не видно?».
Чтобы мозг не перегревался (а компилятор не начинал устраивать вам Unresolved reference), в Kotlin есть два механизма порядка:
- Visibility modifiers (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]} 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 ограничивать их жизнь.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ