Kotlin - Void vs. Unit vs. Nothing: лучшие практики в 2025
Привет, коллеги! Если вы когда-нибудь писали на Kotlin и задумывались: "А почему тут Unit, а не void? И что за зверь такой Nothing?" — вы попали по адресу. Эта тема — настоящий хит на Stack Overflow с тысячами просмотров, и не зря. Путаница между Void, Unit и Nothing способна сбить с толку даже опытного разработчика. В 2025 году, когда Kotlin уверенно занимает нишу в backend (Ktor, Spring) и Android, понимание этих типов — не просто прихоть, а необходимость для написания чистого, безопасного и выразительного кода.
В этой статье мы не просто перечислим определения. Мы разберем их на реальных примерах, покажем, где каждый из них уместен, а где — смертный грех. Поехали!
Коротко о главном: что есть что?
Давайте сразу расставим точки над i. Эти три типа решают разные задачи, хотя на первый взгляд кажутся похожими.
Void— это пережиток Java. В Kotlin он существует только для совместимости. Никогда не используйте его в чистом Kotlin-коде.Unit— это "я ничего не возвращаю, но я все равно тип". Аналогvoidв Java, но лучше. Используется в 99% случаев, когда функция не возвращает значимого результата.Nothing— это "функция никогда не завершится нормально". Она либо выбросит исключение, либо уйдет в бесконечный цикл. Это мощный инструмент для управления потоком выполнения.
Звучит просто? Давайте копать глубже.
Void — чужой среди своих
Если вы пишете на чистом Kotlin, забудьте про Void как про страшный сон. Это класс из Java (java.lang.Void), который является оберткой для ключевого слова void. Он существует в Kotlin исключительно для взаимодействия с Java-кодом.
Пример, когда Void может появиться в вашем проекте:
import java.lang.Void
// Никогда так не делайте в чистом Kotlin!
fun doSomething(): Void? {
println("Делаю что-то...")
return null // Обязательно нужно вернуть null, иначе ошибка компиляции
}
Видите этот return null? Это главный минус. Void — это нельзя создать экземпляр (у него приватный конструктор), но он может быть null. Это порождает путаницу и лишний код.
Лучшая практика 2025: Используйте Void только если вы переопределяете метод из Java-библиотеки, который требует Void в качестве дженерик-параметра. Например, в Callable<Void>. Во всех остальных случаях — Unit.
Unit — король пустых функций
Unit — это истинный Kotlin-способ сказать: "Функция выполнена, но результат меня не интересует". В отличие от void, Unit — это полноценный тип с одним-единственным значением — Unit.
Это дает крутые возможности:
- Дженерики. Вы можете использовать
Unitв обобщенных функциях. - Функции высшего порядка. Вы можете передавать функцию, возвращающую
Unit, как аргумент. - Расширения. Можно писать extension-функции, которые возвращают
Unit.
Стандартный пример — лямбда-выражения:
fun executeAction(action: () -> Unit) {
println("Готовимся к действию...")
action() // Вызываем переданную функцию
println("Действие выполнено.")
}
fun main() {
// Лямбда, которая ничего не возвращает, но делает сайд-эффект
executeAction {
println("Привет из лямбды!")
}
}
Здесь () -> Unit — это сигнатура функции, которая принимает ноль аргументов и возвращает Unit. Без Unit мы бы не смогли так элегантно описать "пустую" лямбду.
Еще один важный момент: возвращать Unit можно неявно. Компилятор сам подставит return Unit, если вы ничего не пишете.
fun logMessage(message: String): Unit {
println("LOG: $message")
// return Unit — неявно
}
Лучшая практика: Используйте Unit для всех функций, которые выполняют действие (сайд-эффект), но не возвращают данных. Это стандарт де-факто в Kotlin-сообществе.
Nothing — джокер в колоде типов
А вот и самый интересный персонаж. Nothing — это тип, который не имеет экземпляров. Вы не можете создать переменную типа Nothing (кроме null). Зачем он нужен?
Nothing используется, чтобы обозначить, что выполнение кода никогда не дойдет до конца функции. Это происходит в двух случаях:
- Функция всегда выбрасывает исключение.
- Функция содержит бесконечный цикл.
Классический пример — функция todo() или error() из стандартной библиотеки:
fun fail(message: String): Nothing {
throw IllegalStateException(message)
// После throw выполнение не продолжается
}
fun main() {
val userInput: String? = null
val result = userInput ?: fail("Input cannot be null")
// Компилятор знает, что если userInput == null,
// то fail() выбросит исключение, и result будет String, а не String?
println(result.length) // Безопасно!
}
Обратите внимание на возвращаемый тип Nothing. Благодаря ему компилятор понимает, что после вызова fail() программа не продолжит нормальное выполнение. Это позволяет делать smart cast и писать более безопасный код.
Другой пример — бесконечный цикл на сервере:
fun startServer(): Nothing {
while (true) {
// Слушаем запросы
println("Сервер работает...")
Thread.sleep(1000)
}
// return Nothing — никогда не выполнится
}
Лучшая практика: Используйте Nothing для функций, которые гарантированно прерывают нормальный поток (исключения, System.exit(), бесконечные циклы). Это документирует намерение и помогает компилятору.