ПрограммированиеKotlin разработчик

Как реализовано ключевое слово 'inline' применительно к классам (value class/inline class) в Kotlin? Какие ограничения существуют, как работают такие классы на уровне байткода, когда и почему их стоит использовать? Приведите пример и разъясните типичные сложности.

Проходите собеседования с ИИ помощником Hintsage

Ответ

В Kotlin inline class (начиная с Kotlin 1.5 — термин "value class"), позволяют создавать обёртки над типами с минимальными издержками. При компиляции такие классы под капотом подменяются на их внутреннее значение (value) во избежание накладных расходов на создание объектов.

Ограничения и особенности:

  • Может быть только одно свойство в primary constructor.
  • Не допускается хранение ссылок на referential equality (=== не работает как обычно).
  • Value class не может быть наследуемым классом, а также не может иметь состояния кроме своей value.
  • Не все generic-типы и платформенные API могут работать с inline/value class без boxing.
  • Value class не может иметь init-блок, поля кроме value и только минимальные функции.

Пример:

@JvmInline value class UserId(val value: String) fun getUser(id: UserId) { println("Loading user with id: ${id.value}") } val id = UserId("XYZ") getUser(id) // Под капотом работает просто со String!

Когда использовать:

  • Обеспечение типобезопасности для идентификаторов, специальных значений.
  • Улучшение производительности при работе с миллионами подобных обёрток (не создаются объекты).

Вопрос с подвохом

Можно ли наследовать value class или использовать его в hierarchy интерфейсов/абстрактных классов?

Ответ: Нет, value class не может наследовать другие классы (исключая интерфейсы), не может быть открыт для наследования, не допускает init-блок и других нестатических полей. Единственный доступный вариант — реализовывать интерфейсы.

Пример:

interface Validatable { fun isValid(): Boolean } @JvmInline value class Email(val raw: String) : Validatable { override fun isValid() = raw.contains("@") }

Примеры реальных ошибок из-за незнания тонкостей темы


История

Android-приложение резко увеличило время старта после добавления value class к параметрам Parcelable: выяснилось, что некорректный @Parcelize с value class приводил к boxing/unboxing на каждом этапе сериализации, сбивая преимущества inline.


История

Микросервис начал активно использовать value class для UserId и ProductId ради типобезопасности, но во многих местах дженериковые функции требовали рефлексии, которая не работала с "оберткой". Unit-тесты неожиданно начали падать, появились ClassCastException.


История

Мигрированный с Java код стал заменять внутренние доменные классы на value class для оптимизации, но их использование в качестве nullable-полей приводило к неожиданным nulll pointer exception, ведь value class может быть null только если наружное значение тоже null, а это ломало старые инварианты.