Всем привет, меня зовут Сергей Прощаев. Я техлид в FinTech, преподаю на курсах в Otus и продолжаю нашу серию «Kotlin для новичков».

В прошлый раз мы разобрались с условиями и циклами. Научились управлять программой, заставляли её принимать решения. Но что, если программа разрастается? Представьте, что вся логика кредитного скоринга, которую мы обсуждали в прошлой статье, написана в одном main(). Это будет монстр на 500–1000 строк. В такой «простыне» невозможно ориентироваться.

Сегодня разберём ключевую тему. Мы научимся делить программу на кирпичики — функции. Речь не только о синтаксисе. Это про то, как перестать писать «спагетти‑код» и начать строить системы, которые легко читать, тестировать и развивать.

1. Зачем нужны функции? (Или история про шкаф)

В начале своей карьеры я работал в банке, где один из модулей банковского приложения представлял собой файл более двух тысяч строк. Всё это было свалено в кучу. Чтобы поменять формулу расчёта процента, нужно было найти нужную строчку среди сотен if-else. Страх сломать что‑то был настолько велик, что разработчики просто копировали код, создавая новые баги.

Функции — это способ разложить программу по полочкам. Как в шкафу: носки в одном ящике, рубашки — в другом. Когда нужно найти носки, вы не перерываете весь шкаф, вы знаете, где лежит нужный ящик.

В программировании это называется декомпозиция. Мы разбиваем большую задачу на маленькие, логически завершенные подзадачи.

2. Как объявлять функции: простой синтаксис

В Java вы привыкли писать public static void main. В Kotlin всё короче и ближе к математике.

Базовая структура выглядит так:fun (сокращение от function) → имя функции → аргументы (входные данные) → возвращаемый тип.

package ru.otus

// Простая функция: принимает имя, возвращает приветствие
fun greetUser(name: String): String {
    return "Привет, $name! Добро пожаловать в OTUS."
}

fun main() {
    val message = greetUser("Алексей")
    println(message)
}

Важный нюанс (мой любимый): если функция состоит из одного выражения, можно убрать фигурные скобки и return. Это называется single‑expression function.

// То же самое, но лаконично
fun greetUserShort(name: String): String = "Привет, $name!"

Компилятор Kotlin настолько умен, что часто может вывести тип возвращаемого значения сам, если вы используете короткую форму. Но в публичных API рекомендую тип указывать явно — это делает код самодокументируемым.

3. Именованные аргументы и значения по умолчанию

Значения по умолчанию — это те фишки, за которые я полюбил Kotlin больше всего. В Java, если у вас есть метод с 5 параметрами (хотя в лучших практиках рекомендуют не более трех), и вам нужно передать только 2, вы создаете перегрузку или передаете null. Kotlin решает это элегантно.

Представьте, что мы пишем функцию отправки уведомления:

fun sendNotification(
    userId: String,
    message: String,
    priority: Int = 1,        // Значение по умолчанию
    isSilent: Boolean = false, // Значение по умолчанию
    retryCount: Int = 0        // Значение по умолчанию
) {
    println("Отправка $userId: $message (приоритет $priority, без звука: $isSilent)")
}

Теперь, вызывая эту функцию, мы можем указывать только то, что нам нужно:

fun main() {
    // Используем все дефолты
    sendNotification("user123", "Привет!")

    // Меняем только приоритет, используя именованный аргумент
    sendNotification("user456", "Срочно!", priority = 5)

    // Меняем несколько параметров. Порядок больше не важен!
    sendNotification(
        userId = "user789",
        message = "Важное обновление",
        isSilent = true,
        priority = 10
    )
}

Best Practice: В команде мы договорились всегда использовать именованные аргументы, если в функции больше 2–3 параметров одного типа (и если параметров более трех — то пересматриваем дизайн класса, минимизируя до двух). Это спасает от путаницы, когда вы видите sendNotification("id", "text", 5, true). Глядя на такой код, сразу не поймешь, что значат 5 и true. Именованные аргументы делают код самодокументируемым.

4. Стек вызовов (Call Stack): что происходит под капотом

Когда вы вызываете функцию из функции, Kotlin (как и Java) запоминает, куда вернуться. Это называется стек вызовов.

Давайте визуализируем это. Допустим, у нас есть три функции: main() вызывает calculate(), а calculate() вызывает multiply().

Рисунок 1. Схема стека вызовов: каждая новая функция кладется на вершину стека. Когда функция завершается, она снимается со стека, и управление возвращается к вызывающей функции.
Рисунок 1. Схема стека вызовов: каждая новая функция кладется на вершину стека. Когда функция завершается, она снимается со стека, и управление возвращается к вызывающей функции.

Для начинающего разработчика важно понимать: когда функция заканчивается (доходит до return или закрывающей скобки), все локальные переменные этой функции «умирают» (освобождаются). Это называется область видимости (scope).

5. Области видимости (Scopes): где живет переменная

Переменные живут ровно столько, сколько живет функция, в которой они объявлены.

fun outerFunction() {
    val x = 10 // x доступна только здесь

    fun innerFunction() {
        val y = 20 // y доступна только здесь
        // Внутренняя функция видит x из внешней
        println(x + y)
    }

    // Здесь y уже недоступна!
    innerFunction() // А вот эту вызвать можно
}

fun main() {
    // x и y здесь НЕдоступны
    outerFunction()
}

Кейс из практики: Однажды стажер в нашей команде пытался использовать переменную, объявленную внутри if, во всей остальной функции. Компилятор выдавал ошибку «Unresolved reference». Мы объяснили ему про scope. Это базовая дисциплина, которая учит нас не «мусорить» в памяти и четко разделять, где какие данные нужны.

6. Функция main() — точка входа

Мы постоянно её используем, но не акцентируем внимание. main() — это дверь, через которую JVM входит в вашу программу. В Kotlin она может быть как с аргументами (для обработки параметров командной строки), так и без.

// Простейший вариант
fun main() {
    println("Старт приложения")
}

// С аргументами (если вы запускаете программу через консоль: java -jar app.jar arg1 arg2)
fun main(args: Array<String>) {
    println("Первый аргумент: ${args.getOrNull(0)}")
}

7. Функциональная декомпозиция: как это работает в реальной команде

Теперь давайте соберем всё вместе и посмотрим, как мы пишем код в промышленной разработке. Мы не пишем «кирпичи», мы строим «систему».

Задача: Написать скрипт, который рассчитывает бонус сотруднику. Бонус зависит от стажа и должности.

Плохой подход (монолит):
Всё в main(): запрос данных, логика расчета, форматирование, вывод.

Хороший подход (декомпозиция):
Разбиваем на логические функции. Каждая делает одно дело.

package ru.otus

import java.time.Year

// 1. Чисто логика расчета
fun calculateBonus(yearsOfService: Int, isManager: Boolean): Double {
    val baseBonus = if (yearsOfService > 5) 1000.0 else 500.0
    val multiplier = if (isManager) 1.5 else 1.0
    return baseBonus * multiplier
}

// 2. Форматирование результата для вывода
fun formatBonusMessage(employeeName: String, bonus: Double): String {
    return "Сотрудник $employeeName, ваш бонус: %.2f рублей".format(bonus)
}

// 3. Функция для ввода данных (здесь мы оборачиваем работу с консолью)
fun readEmployeeInfo(): Pair<String, Int> {
    println("Введите имя сотрудника:")
    val name = readln()
    println("Введите стаж (лет):")
    val experience = readln().toIntOrNull() ?: 0
    return Pair(name, experience)
}

fun main() {
    // Точка входа только координирует работу других функций
    val (name, experience) = readEmployeeInfo()

    // Допустим, мы знаем, что это менеджер (в реальности мы бы это тоже где-то брали)
    val isManager = name.contains("Руководитель", ignoreCase = true)

    val bonus = calculateBonus(experience, isManager)
    val message = formatBonusMessage(name, bonus)

    println(message)
}

Что мы получили?

  1. Тестируемость: Я могу написать тест для calculateBonus(3, true) и проверить, правильно ли считается бонус, без запуска всей программы.

  2. Переиспользование: formatBonusMessage можно использовать в других местах приложения (например, при отправке email).

  3. Читаемость: main() читается как инструкция: прочитали данные → посчитали бонус → вывели.

Заключение и путь вперёд

Сегодня мы разобрали, как Kotlin делает работу с функциями удобной и безопасной:

  • Декомпозиция — разбиваем «монолит» на кирпичи.

  • Аргументы по умолчанию и именованные параметры — пишем чистые вызовы.

  • Call Stack и Scope — понимаем, как живут данные.

  • Best Practice — одна функция решает одну задачу.

Это основа, на которой строится вся архитектура приложений. В следующих статьях мы поговорим о функциональном программировании (лямбды, функции высшего порядка) и перейдем к самому интересному — работе с коллекциями.

Весь код из этой статьи доступен в моём репозитории на GitHub 

Если вы чувствуете, что вам нужна система, а не разрозненные статьи, если хотите научиться применять эти паттерны в реальных проектах (бэкенд, мультиплатформа) — приходите на курс «Kotlin‑разработчик. Базовый уровень» в OTUS. Мы разбираем не только синтаксис, но и архитектуру, корутины, многопоточность и другие темы, необходимые промышленному разработчику.

Серия статей «Kotlin для новичков»:

  1. Kotlin для новичков: от установки IDE до первого проекта

  2. Kotlin для новичков: переменные и базовые операции — полный гайд 2026

  3. Kotlin для новичков: всё об условиях и циклах за 15 минут 

А если хочется сначала посмотреть, как это выглядит вживую — можно начать с открытых уроков:

  • 28 апреля 20:00 «Контрактные тесты в Kotlin: как подружить фронт и бэкэнд». Записаться

  • 6 мая 19:00 «Разработка проекта на Kotlin: коллаборация человека, архитектурных шаблонов и ИИ‑команды». Записаться

Они дают хороший срез того, как базовые вещи вроде функций и декомпозиции превращаются в реальные инженерные решения.