1. Функція як вираз
Коли ви починаєте активно виносити код у функції, трапляється типова історія: файл стає приємнішим, main() — коротшим, але… функцій зʼявляється багато. І серед них є такі, які за змістом цілком доречні, а за розміром виглядають як «пʼять рядків заради однієї думки». Надто багато дрібних функцій може бути незручним.
З іншого боку, якщо залишити все в одному main, отримаємо «простирадло». Тож потрібен баланс: маленькі функції — це добре, але «код заради коду» інколи лише заважає. У Kotlin є два інструменти, які допомагають утримувати цей баланс: expression body (коротке тіло функції через =) і локальні функції (функції всередині інших функцій).
Expression body
Expression body — це спосіб сказати Kotlin: «тіло цієї функції складається з одного виразу, тож запишімо її акуратно — без фігурних дужок і return». Припустімо, нам потрібна функція для піднесення до квадрата. Ось її класичний запис:
fun square(x: Int): Int {
return x * x
}
Але ми можемо записати її й коротше:
fun square(x: Int): Int = x * x
Зміст той самий, але запис ближчий до математичної формули. Для простих обчислень і перевірок це часто читається краще: функція виглядає як визначення, а не як міні‑сценарій.
Міні‑приклад 1: обчислення x у кубі
fun cube(x: Int): Int = x * x * x
fun main() {
println(cube(3)) // 27
}
Міні‑приклад 2: перевірка на парність
fun isEven(x: Int): Boolean = x % 2 == 0
fun main() {
println(isEven(10)) // true
println(isEven(7)) // false
}
Тут важливо відчути суть: isEven читається як запитання, а праворуч — «відповідь‑формула». Для початківців це, чесно кажучи, один із найприємніших способів читати код.
2. Expression body: виведення типу і коли краще блочне тіло
Expression body має приємний бонус: Kotlin часто сам визначає тип, який повертається, за виразом праворуч. Тобто можна писати ще коротше:
fun square(x: Int) = x * x
І компілятор виведе тип як Int.
Але варто бути уважними: інколи явний тип — це не «зайва писанина», а турбота про того, хто читатиме код. Тож домовмося про просте правило: якщо функція цілком очевидна — тип можна опустити; якщо є бодай тінь сумніву — краще залишити.
Порівняймо в таблиці:
| Варіант | Приклад | Коли використовувати |
|---|---|---|
| Expression body + явний тип | |
Коли хочете, щоб контракт читався одразу |
| Expression body + виведення типу | |
Коли вираз максимально очевидний |
| Блочне тіло | |
Коли є кілька кроків, if, цикл або змінні |
Міні‑приклад: рядок як результат
fun formatMoney(amount: Int): String = "$amount €"
fun main() {
println(formatMoney(120)) // 120 €
}
Коли expression body краще не використовувати
Expression body має пастку: раз можна коротше — значить «треба» коротше. У результаті код перетворюється на однорядкових монстрів, де половина логіки ховається в дужках. Читати таке хочеться хіба що під загрозою дедлайну.
Якщо всередині функції зʼявляється кілька кроків, проміжні змінні, цикл або розгалуження, то блочне тіло зазвичай логічніше: воно показує, що тут не формула, а процес.
Порівняйте відчуття. Так — нормально й читабельно:
fun absInt(x: Int): Int {
if (x < 0) return -x
return x
}
Так — уже спірно для новачка (хоча технічно коректно):
fun absInt(x: Int): Int = if (x < 0) -x else x
3. Міні‑застосунок: «Cкарбничка витрат»
Щоб не обговорювати компактність у вакуумі, продовжимо розвивати консольний застосунок. Нехай це буде проста «cкарбничка витрат»: користувач вводить кілька витрат (сум), а програма рахує підсумок, максимум і середнє.
Спочатку накидаємо простий каркас (ніби він уже був у попередніх лекціях), а потім почнемо його «причісувати».
fun main() {
val amounts = intArrayOf(120, 80, 200, 50)
val total = sumAmounts(amounts)
val max = maxAmount(amounts)
val avg = averageAmount(amounts)
println("Разом: $total") // Разом: 450
println("Максимум: $max") // Максимум: 200
println("Середнє: $avg") // Середнє: 112.5
}
Функції поки зробимо звичайними, щоб було з чим працювати:
fun sumAmounts(xs: IntArray): Int {
var total = 0
for (x in xs) {
total += x
}
return total
}
fun maxAmount(xs: IntArray): Int {
var m = xs[0]
for (x in xs) {
if (x > m) m = x
}
return m
}
fun averageAmount(xs: IntArray): Double {
val total = sumAmounts(xs)
return total.toDouble() / xs.size
}
Тепер подивімося очима «редактора коду»: які функції тут — кандидати на expression body? averageAmount — так, бо в ній логіка майже формульна (хоча є проміжна змінна). Можна зробити трохи компактніше, але без фанатизму.
Охайний варіант, який усе ще легко читати:
fun averageAmount(xs: IntArray): Double = sumAmounts(xs).toDouble() / xs.size
І тут добре видно головну перевагу: ми не «стиснули код заради стискання», а зробили функцію схожою на визначення середнього.
Оновімо приклад повністю (зверніть увагу: ми залишили sumAmounts і maxAmount блочними, бо там є цикл, а це вже не «один вираз»):
fun averageAmount(xs: IntArray): Double = sumAmounts(xs).toDouble() / xs.size
fun main() {
val amounts = intArrayOf(120, 80, 200, 50)
println(averageAmount(amounts)) // 112.5
}
4. Локальні функції в застосунку
Іноді функція потрібна лише як маленький помічник усередині одного сценарію. Виносити її на рівень файла — означає «засмічувати простір імен»: зʼявляється ще одне імʼя, ще одна річ, яку можна випадково використати не там, де треба. Локальна функція розвʼязує це: ви оголошуєте fun helper(...) прямо всередині іншої функції, і вона існує лише там.
Це схоже на те, як ви заводите локальну змінну: ви ж не оголошуєте кожну тимчасову змінну глобально на весь файл. Так само й із локальними функціями: вони «живуть поруч із місцем використання» й не заважають іншим.
Важливий момент: локальна функція може звертатися до змінних зовнішньої функції й навіть змінювати їх (тобто використовувати «зовнішнє оточення»). Це працює так само, як і в інших вкладених конструкціях: вони можуть захоплювати змінні із зовнішньої області видимості (scope).
Введення з парсингом і валідацією
Зробімо наступний крок у «копілці витрат»: замість жорстко заданого масиву читатимемо витрати з консолі. Але тут одразу зʼявляється типовий сценарій введення: прочитати рядок, прибрати зайві пробіли, розпарсити число, перевірити, що воно додатне. І такий сценарій часто або дублюється, або стає надто довгим усередині main.
Зробімо функцію readPositiveInt(prompt: String): Int, а всередині неї сховаємо локальних помічників.
fun readPositiveInt(prompt: String): Int {
fun parseIntOrNull(text: String): Int? {
val cleaned = text.trim()
return cleaned.toIntOrNull()
}
while (true) {
print(prompt)
val raw = readln()
val value = parseIntOrNull(raw)
if (value != null && value > 0) return value
println("Введіть ціле число, більше за 0.")
}
}
Тут локальна функція parseIntOrNull не потрібна всій програмі — вона потрібна лише для цього введення. Тому тримати її всередині логічно: ви читаєте readPositiveInt і одразу бачите, як саме ми парсимо рядок.
Невелика блок‑схема процесу введення
flowchart TD
A[Показати prompt] --> B[Прочитати рядок]
B --> C[trim]
C --> D[toIntOrNull]
D --> E{value != null і value > 0?}
E -- так --> F[return value]
E -- ні --> G[Повідомлення про помилку]
G --> A
Тепер використаємо цю функцію в main і зберемо масив витрат фіксованого розміру. Розмір теж запитаємо в користувача — так буде ближче до реального застосунку.
fun main() {
val n = readPositiveInt("Скільки витрат увести? ")
val amounts = IntArray(n)
for (i in 0 until n) {
amounts[i] = readPositiveInt("Витрата #${i + 1}: ")
}
println("Разом: ${sumAmounts(amounts)}") // наприклад: Разом: 450
}
Комбінуємо підходи: helpers і expression body
Найприємніше починається тоді, коли ви поєднуєте інструменти. Локальні функції допомагають сховати одноразові деталі, а expression body робить «формульні» функції короткими й читабельними.
Наприклад, у нашому застосунку можна зробити маленьку функцію форматування суми — і вона ідеально підходить для expression body. Причому це корисно навіть зараз, коли здається: «та я й так можу написати $amount € прямо в println». Можете. Але потім ви захочете змінити формат (наприклад, додати слово «євро») і почнете шукати по файлу всі місця, де друкували суму.
fun formatMoneyEuro(amount: Int): String = "$amount €"
А всередині main:
fun main() {
val amounts = intArrayOf(120, 80, 200, 50)
val total = sumAmounts(amounts)
println("Разом: ${formatMoneyEuro(total)}") // Разом: 450 €
}
Ще один хороший кандидат — «середнє». Ми вже це робили:
fun averageAmount(xs: IntArray): Double = sumAmounts(xs).toDouble() / xs.size
Тепер трохи поліпшимо підсумковий вивід, але обережно: без громіздкого форматування й без нових тем.
fun printReport(amounts: IntArray) {
val total = sumAmounts(amounts)
val max = maxAmount(amounts)
val avg = averageAmount(amounts)
println("Разом: ${formatMoneyEuro(total)}") // Разом: 450 €
println("Максимум: ${formatMoneyEuro(max)}") // Максимум: 200 €
println("Середнє: $avg") // Середнє: 112.5
}
І main стає «сценарієм», а не «мішком логіки»:
fun main() {
val n = readPositiveInt("Скільки витрат увести? ")
val amounts = IntArray(n)
for (i in 0 until n) {
amounts[i] = readPositiveInt("Витрата #${i + 1}: ")
}
printReport(amounts)
}
Ось тут якраз видно, навіщо все це: main читається як послідовність кроків, а деталі розкладені по функціях. Водночас ми не плодимо зайвих «одноразових» функцій на рівні файла: те, що потрібно лише всередині введення, живе локально.
5. Типові помилки при expression body і локальних функціях
Помилка № 1: перетворювати expression body на «компресор мозку».
Іноді хочеться запакувати в = усе підряд — включно зі складними умовами та багатоповерховими обчисленнями. Формально компілятор це перетравить, але людина — не завжди. Якщо рядок перестає читатися «з першого погляду», краще повернутися до блочного тіла й чесно розписати кроки.
Помилка № 2: намагатися зробити expression body там, де потрібен цикл або кілька дій.
Expression body — це про один вираз. Якщо у вас зʼявився for, накопичення суми, оновлення змінної, друк проміжних значень — це вже сценарій, і йому пасує звичайне { ... }. Не сприймайте блочне тіло як «гірше»: воно просто для іншого типу задач.
Помилка № 3: виносити назовні функції, які потрібні рівно в одному місці.
Часто новачок робить десяток маленьких функцій‑помічників на рівні файла, а потім сам же плутається, що й де використовується. Локальна функція — хороший компроміс: вона дає імʼя кроку, але не роздуває зовнішній API файла. Якщо помічник використовується лише всередині однієї функції введення — нехай він і живе там.
Помилка № 4: надмірно ускладнювати локальні функції й ховати важливу логіку надто глибоко.
Локальна функція має бути маленькою й «до діла». Якщо ви всередині readPositiveInt оголосили 5 локальних функцій, і кожна — по 20 рядків, це вже не «сховали деталі», а «зробили квест». У такому разі краще винести частину на рівень файла й дати їй нормальне імʼя.
Помилка № 5: випадково покладатися на зовнішні змінні й отримувати несподівані ефекти.
Локальні функції можуть звертатися до змінних зовнішньої функції й змінювати їх — це зручно, але може стати пасткою, якщо ви не помітили, що помічник змінює стан. Зробіть такі зміни очевидними: якщо помічник щось змінює, це має читатися з коду, а не бути сюрпризом.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ