1. Значения по умолчанию
Когда вы только начинаете, кажется, что проблема «перепутал параметры местами» — это что-то из мира больших проектов. Но на практике она приходит очень рано: стоит появиться функции с 3–5 параметрами одного типа (например, несколько Int или несколько Boolean), как мозг начинает нас обманывать. И это не «вы невнимательные», это нормальная человеческая биология: у нас нет встроенного парсера сигнатур.
Решение в Kotlin простое и эффективное: часть параметров можно сделать необязательными через значения по умолчанию, а при вызове можно подписывать аргументы именами. Это превращает вызов функции из волшебного заклинания в понятную фразу, которую можно читать как обычный текст.
Делаем параметр необязательным
Давайте зайдем с простого: очень часто функция вызывается типично, и только иногда хочется уточнить деталь. Например, «напечатай заголовок» почти всегда печатает линию = одной и той же ширины — но иногда хочется шире или другим символом. В таких случаях значение по умолчанию — это как настройка, которую обычно трогать не нужно.
Синтаксис очень простой: в объявлении параметра пишем = …. Kotlin использует это значение, если при вызове не передали аргумент для этого параметра.
fun printHeader(title: String, width: Int = 30, border: Char = '=') {
val line = border.toString().repeat(width)
println(line)
println(title)
println(line)
}
fun main() {
printHeader("Кошелёк") // печатает ширину 30 и '='
printHeader("Кошелёк", width = 10) // печатает ширину 10 и '='
}
Обратите внимание на важный момент: мы не делаем «две версии» функции. Функция одна, но у неё есть «настройки по умолчанию». Это почти всегда лучше для проекта, чем плодить много похожих функций с чуть разными именами.
Дефолт — это не только константа
Может казаться, что значение по умолчанию должно быть чем-то простым вроде 0, true или "text". Но Kotlin разрешает использовать любое выражение, в том числе зависящее от других параметров. Это очень удобно: например, «длина по умолчанию равна размеру массива», или «лимит по умолчанию равен длине строки».
Важно только правило: если дефолт зависит от другого параметра, этот «другой параметр» должен стоять раньше в списке параметров (логично: нельзя ссылаться на то, чего ещё «нет»).
Небольшой пример на нашем проекте «Кошелек»: хотим напечатать список расходов, и по умолчанию печатать всего списка.
fun printExpenses(amounts: IntArray, size: Int, limit: Int = size) {
var i = 0
while (i < size && i < limit) {
println("Расход #${i + 1}: ${amounts[i]}") // например: Расход #1: 150
i++
}
}
fun main() {
val amounts = intArrayOf(150, 40, 999)
val size = 3
printExpenses(amounts, size) // печатает 3 расхода
printExpenses(amounts, size, limit = 1) // печатает 1 расход
}
Порядок параметров: обязательные вперёд, дефолтные назад
Теперь важная, но логичная мысль. Значения по умолчанию делают параметр необязательным — и значит, его можно не указывать. Поэтому такие параметры всегда удобнее ставить ближе к концу списка, а обязательные — ближе к началу. Чтобы компилятор точно знал, где и какой аргумент передали.
Kotlin имеет специальное правило: если параметр с дефолтом стоит перед параметром без дефолта, то при вызове вы сможете опустить этот параметр только используя именованный аргумент.
Сделаем пример «как лучше не делать»:
fun greeting(userId: Int = 0, message: String) {
println("[$userId] $message")
}
fun main() {
greeting(message = "Привет!") // [0] Привет!
greeting("Привет!") // так нельзя: компилятор будет ругаться
}
Такой порядок иногда нужен (бывает), но чаще это просто кривой дизайн. В любых задачах и в нашем приложении лучше придерживаться простого правила: «самое важное — сначала, настройки — потом».
Например, вот более приятный вариант:
fun greeting(message: String, userId: Int = 0) {
println("[$userId] $message")
}
fun main() {
greeting("Привет!") // [0] Привет!
greeting("Привет!", 42) // [42] Привет!
}
2. Именованные аргументы
Подписываем аргументы и перестаём бояться одинаковых типов
Именованные аргументы нужны не потому, что Kotlin любит многословность, а потому что они резко снижают вероятность ошибки. Особенно когда параметры одинакового типа или когда значение само по себе не объясняет смысл (классика: набор аргументов true, false, true — «что это было?»).
Синтаксис: при вызове пишем имяПараметра = значение. Kotlin позволяет перечислять именованные аргументы в любом порядке — именно ради читабельности.
Сделаем функцию для добавления расхода в массив (пока без категорий, чтобы не забегать вперёд). Пусть у нас есть «дата» как день и месяц — примитивно, но для примера идеально.
fun addExpense(
amounts: IntArray,
days: IntArray,
months: IntArray,
size: Int,
amount: Int,
day: Int,
month: Int
): Int {
amounts[size] = amount
days[size] = day
months[size] = month
return size + 1
}
fun main() {
val amounts = IntArray(10)
val days = IntArray(10)
val months = IntArray(10)
var size = 0
size = addExpense(amounts, days, months, size, amount = 150, day = 14, month = 1)
println(size) // 1
}
Если бы мы вызвали это позиционно как addExpense(…), это выглядело бы как «телефонный номер». С именами смысл виден сразу: amount, day, month.
Как менять настройку и не передавать лишнее
Частый сценарий: у функции несколько параметров с дефолтами, и вы хотите поменять не первый из них, а какой-то другой. Kotlin это разрешает, и тут полезно запомнить простое правило: если вы пропускаете дефолтные параметры, то то, что вы задаёте дальше, обычно проще (и безопаснее) задавать именованно.
Документация формулирует это так: вы можете пропустить часть аргументов с дефолтами, но после первого пропущенного аргумента нужно именовать все последующие (иначе непонятно, что к чему относится).
Пример: функция печати отчёта. По умолчанию печатаем и список, и сумму, а валюта — "EUR". Иногда хочется поменять только валюту, не трогая остальные настройки.
fun printReport(
title: String,
showList: Boolean = true,
showTotal: Boolean = true,
currency: String = "EUR"
) {
println("== $title ==")
println("showList=$showList, showTotal=$showTotal, currency=$currency")
}
fun main() {
printReport("Отчёт") // == Отчёт == / showList=true, showTotal=true, currency=EUR
printReport("Отчёт", currency = "USD") // == Отчёт == / showList=true, showTotal=true, currency=USD
}
Если бы именованных аргументов не было, нам пришлось бы писать что-то вроде printReport("Отчёт", true, true, "USD") — и снова угадывать, какие там были два true.
Смешиваем позиционные и именованные аргументы
На практике часто получается «гибридный стиль»: первые 1–2 параметра очень понятны по смыслу (обычно title, text, amount), и их удобно передавать позиционно, а вот дальше начинаются настройки, и их лучше передавать именованно. Kotlin это поддерживает.
Обычно придерживаются простого правила для читаемости: сначала идут позиционные аргументы, а потом — именованные. Такой вызов выглядит аккуратно, как «основная мысль + уточнения».
Давайте применим это к нашему заголовку:
fun printHeader(title: String, width: Int = 30, border: Char = '=') {
val line = border.toString().repeat(width)
println(line)
println(title)
println(line)
}
fun main() {
printHeader("Кошелёк", border = '#') // сначала title, потом настройка
printHeader("Кошелёк", width = 10, border = '*')
}
Такой стиль особенно полезен, когда вы проектируете «мини-API» для себя: базовые параметры читаются быстро, а редкие настройки не мешают, но всегда доступны.
4. Практика: меню «Кошелёк»
Мини-рефакторинг: делаем меню расширяемым
Теперь соберём небольшой, но цельный кусочек нашего консольного приложения «Кошелёк». Мы не добавляем ничего принципиально нового в логике; мы улучшаем интерфейс функций: делаем параметры по умолчанию и используем именованные аргументы, чтобы main() читался как сценарий.
У нас будет стандартное меню: показать команды, добавить расход, вывести список, вывести отчёт. Мы ещё не делаем «идеальный парсинг» ввода (это будет в следующих уровнях), поэтому пусть будет простое чтение строкой и простые проверки.
fun printMenu(appName: String = "Кошелёк", showHint: Boolean = true) {
printHeader(appName, width = 25)
println("1) Добавить расход")
println("2) Показать расходы")
println("3) Показать сумму")
println("0) Выход")
if (showHint) println("Введите номер команды и нажмите Enter") // Введите номер команды...
}
fun printHeader(title: String, width: Int = 30, border: Char = '=') {
val line = border.toString().repeat(width)
println(line)
println(title)
println(line)
}
fun main() {
printMenu() // дефолтное имя и подсказка
printMenu(showHint = false) // тот же экран, но без подсказки
printMenu(appName = "Budget v0") // кастомное имя
}
Здесь важна сама идея: мы сделали поведение по умолчанию «типичным» для большинства вызовов, а редкие настройки вынесли в параметры. И из-за именованных аргументов printMenu(showHint = false) выглядит как читаемая фраза.
Памятка: дефолты и именование
В этот момент обычно хочется спросить: «Окей, а как понять, что лучше: дефолт или именование?» Хорошая новость: тут есть практические критерии. Дефолты — это способ сделать «обычный путь» короче, а именованные аргументы — способ сделать «необычный путь» безопаснее и понятнее.
| Ситуация | Что лучше использовать | Почему |
|---|---|---|
| Параметр почти всегда одинаковый | Значение по умолчанию | Убираем шум из большинства вызовов |
| Несколько параметров одного типа (Int, Boolean, Char) | Именованные аргументы | Снижаем шанс перепутать порядок |
| Нужно поменять «настройку в конце», не трогая остальные | Дефолты + именование нужной настройки | Не приходится передавать «лишние» аргументы |
| Вызов плохо читается без расшифровки | Именованные аргументы | Вызов становится самодокументируемым |
И да, иногда правильный ответ — «функция перегружена параметрами и её надо упростить». Но это уже ощущение, которое придёт чуть позже, когда функций станет много.
Схема решения: делать ли параметр дефолтным
Чтобы закрепить подход, вот простая блок-схема, по которой удобно принимать решение в своих проектах:
flowchart TD
A[Есть функция с параметром P] --> B{P обязателен всегда?}
B -->|Да| C[Оставляем без дефолта]
B -->|Нет| D{Есть безопасное ожидаемое значение?}
D -->|Да| E[Делаем дефолт: P: Type = ...]
D -->|Нет| F[Оставляем обязательным, но вызываем именованно]
E --> G{Параметры одного типа и легко перепутать?}
F --> G
G -->|Да| H[Рекомендуем именованные аргументы]
G -->|Нет| I[Можно позиционно, если читаемо]
Слово «безопасное» здесь ключевое. Если дефолт может привести к странному поведению (например, дефолтный month = 0 — а у нас месяцы с 1), то лучше заставить явно передавать значение.
5. Типичные ошибки
Ошибка №1: дефолт «на авось», который выглядит удобно, но ломает смысл.
Иногда хочется поставить дефолт, лишь бы параметр стал необязательным. Например, сделать day: Int = 0. Но если 0 — невалидный день, вы создаёте мину замедленного действия: программа будет работать, но данные станут мусорными. Хороший дефолт — это не «любой», а ожидаемый и безопасный.
Ошибка №2: обязательные параметры стоят после дефолтных, и вызовы превращаются в загадки.
Такой дизайн вынуждает постоянно использовать именованные аргументы даже там, где не хотелось бы. Kotlin прямо показывает эту ситуацию: если дефолтный параметр стоит перед обязательным, то опустить его можно только именованным вызовом. В учебных проектах почти всегда проще переставить параметры местами.
Ошибка №3: несколько одинаковых по типу аргументов передаются позиционно, и порядок путается.
Три числа подряд или три булевых флага подряд выглядят одинаково, и ошибка в одном месте очень трудно заметна глазами. В таких вызовах именованные аргументы — это не «болтовня», а страховка.
Ошибка №4: хаотичное смешивание позиционных и именованных аргументов.
Когда часть аргументов передаётся позиционно, часть именованно, а часть снова позиционно (или просто в странном порядке), чтение превращается в боль. Kotlin разрешает именованные аргументы в любом порядке, но цель не в том, чтобы удивить коллегу, а в том, чтобы сделать код очевидным: сначала основное, потом настройки.
Ошибка №5: слишком много параметров с дефолтами как попытка заменить нормальный дизайн.
Если у функции 8 параметров, из них 6 с дефолтами, и вы сами не помните, что означает каждый — это сигнал, что функция делает слишком много. В рамках этого курса мы пока не будем уходить в продвинутые конструкции, но как минимум стоит остановиться и спросить себя: «А нельзя ли разделить действие на две более простые функции?»
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ