Avatar ·

Константы в Kotlin: как правильно их создавать и использовать?

Всем привет! Я только начал изучать Kotlin после Java, и меня смущает тема констант. В Java я привык писать `public static final` для глобальных констант и `final` для локальных. Но в Kotlin всё иначе. Например, я хочу объявить максимальное количество попыток входа в приложении – 5. И константу с именем приложения "MyApp". Вот как я пытался это сделать: ```kotlin // Вариант 1: внутри класса class LoginManager { val MAX_ATTEMPTS = 5 val APP_NAME = "MyApp" } // Вариант 2: на уровне файла val MAX_ATTEMPTS = 5 val APP_NAME = "MyApp" ``` Но я прочитал, что использовать `val` внутри класса для констант – плохая практика, потому что это не настоящие константы времени компиляции. А на уровне файла они всё равно будут привязаны к объекту-компаньону, если класс будет загружен. Ещё я видел в документации слово `const`. Когда его нужно использовать? И что делать, если константа – это не примитив или строка, а, например, список или объект? Подскажите, пожалуйста, каким образом в Kotlin принято объявлять константы: где их хранить (в классе, в объекте-компаньоне, на уровне файла), как называть (UPPER_CASE или lowerCamelCase) и когда нужен модификатор `const`? Очень хочется понять best practices, чтобы не делать глупых ошибок в проекте. <<>>

Правильный подход к константам в Kotlin

Вы совершенно правы: val внутри класса — это не константа времени компиляции, а просто неизменяемое свойство экземпляра. Для настоящих констант в Kotlin есть два основных подхода, и выбор зависит от типа данных.

1. Константы времени компиляции (compile-time constants) — модификатор const

Используйте const val, когда значение известно на этапе компиляции и является примитивным типом или строкой. Такие константы встраиваются прямо в байт-код и не имеют поля в классе.

  • Где объявлять: на уровне файла (top-level) или внутри object (включая companion object).
  • Типы: только String и примитивы (Int, Double, Boolean и т.д.).
  • Именование: UPPER_SNAKE_CASE (как в Java).
// На уровне файла — самый простой и рекомендуемый способ
const val MAX_LOGIN_ATTEMPTS = 5
const val APP_NAME = "MyApp"

// Внутри объекта (например, companion object класса)
class LoginManager {
    companion object {
        const val MAX_ATTEMPTS = 5
        const val APP_NAME = "MyApp"
    }
}

2. Константы для сложных объектов — val в объекте

Если константа — это список, объект, enum или любое другое не-примитивное значение, const использовать нельзя. В этом случае объявляйте val внутри object (или на уровне файла, но обычно object даёт лучшую организацию).

object AppConfig {
    val SUPPORTED_LOCALES = listOf("en", "ru", "de")
    val DEFAULT_TIMEOUT = Duration.ofSeconds(30)
    val ERROR_CODES = mapOf(
        404 to "Not Found",
        500 to "Internal Server Error"
    )
}

Такой объект будет синглтоном, и его поля инициализируются лениво (при первом обращении), что безопасно для сложных структур.

3. Локальные константы внутри функции

Для значений, используемых только внутри одной функции, просто используйте val:

fun calculateDiscount(price: Double): Double {
    val MINIMUM_PRICE = 100.0  // локальная константа
    val DISCOUNT_RATE = 0.1    // локальная константа
    return if (price >= MINIMUM_PRICE) price * DISCOUNT_RATE else 0.0
}

Итоговые рекомендации (best practices):

  • Для примитивов и строк — всегда используйте const val на уровне файла или в companion object. Это даёт максимальную производительность.
  • Для сложных объектов — объявляйте val внутри object (или companion object).
  • Именование — UPPER_SNAKE_CASE для констант (это общепринятый стиль в Kotlin, хотя в официальном style guide допускается и lowerCamelCase для констант, не являющихся частью публичного API).
  • Не используйте val внутри класса для констант — это создаёт поле для каждого экземпляра, что неэффективно.
  • Группируйте константы по смыслу: например, object NetworkConfig, object UiConstants — это улучшает читаемость кода.

Надеюсь, теперь стало понятнее. Главное правило: если значение можно вычислить на этапе компиляции и оно примитив — const. Если нет — val в объекте.

<<>>

Альтернативный взгляд: константы в companion object vs top-level

Первый ответ верен, но стоит обсудить дилему: где именно объявлять константы — на уровне файла (top-level) или внутри companion object класса?

Top-level (на уровне файла) — самый простой способ. Он не требует создания объекта. Однако у него есть недостаток: константы становятся доступными из любого места проекта, что может нарушить инкапсуляцию. Если константа нужна только внутри одного класса, лучше поместить её в companion object этого класса.

Companion object — логически связывает константу с классом. Например, LoginManager.MAX_ATTEMPTS сразу говорит, к чему относится константа. Это предпочтительнее, когда константа тесно связана с бизнес-логикой класса.

class LoginManager {
    companion object {
        const val MAX_ATTEMPTS = 5
        const val APP_NAME = "MyApp"
        val ERROR_CODES = setOf(401, 403)  // не примитив — без const
    }
}

// Использование:
val max = LoginManager.MAX_ATTEMPTS

Мой совет: если константа относится к конкретному классу (например, порог срабатывания валидатора), кладите её в companion object. Если константа глобальная для всего приложения (например, название базы данных) — используйте top-level или отдельный object (например, object DbConstants). Главное — быть последовательным в рамках проекта.