Zero-cost* abstractions in Kotlin — статья с подробным объяснением новой экспериментальной языковой конструкции Kotlin под названием inline-классы.
Одна из ключевых особенностей языка Kotlin — null safety, которая гарантирует, что программист не сможет по ошибке вызвать методы объекта, имеющего значение null, или передать этот объект в качестве аргумента другим методам. Null safety существенно повышает надежность кода, но не защищает от других ошибок программиста.
Допустим, у тебя есть база котов и собак, которых ты идентифицируешь по ID. Также у тебя есть метод getDogById(dogId: Long), который возвращает информацию о собаке с конкретным ID. Очевидно, что, если в качестве ID собаки ты передашь методу ID кошки, это будет ошибкой, которая приведет к неопределенному результату. Но ни среда разработки, ни компилятор не скажут тебе о ней.
РЕКОМЕНДУЕМ:
Хорошие и плохие приемы программирования на Kotlin
Еще во времена Java программисты придумали метод обойти эту проблему с помощью так называемых классов-оберток. Ты просто создаешь класс DogId с единственным полем (ID собаки) и используешь его везде, где раньше использовал тип Long в качестве ID. Все остальное компилятор и среда разработки сделают за тебя: они просто не позволят передать DogId в качестве аргумента функции, которая ожидает CatId, — это ошибка.
Но есть в классах-врапперах одна проблема. Создание объектов не самая дешевая операция. Если ты будешь плодить их на каждый чих, то вскоре заметишь возросшее потребление оперативной памяти и процессорных ресурсов.
И здесь на сцену выходят инлайн-классы. По своей сути инлайн-класс — это класс-враппер с одним параметром, который при компиляции разворачивается в этот параметр, чтобы избежать накладных расходов. Например:
1 2 |
inline class DogId(val id: Long) val dog = getDogById(DogId(100L)) |
Данный код написан с использованием враппера, чтобы избежать описанной выше ошибки. Однако при компиляции объект DogId будет заменен Long, так что никаких дополнительных накладных расходов не потребуется.
Компилятор накладывает следующие ограничения на инлайн-классы:
- не больше одного параметра;
- никаких теневых полей;
- никаких блоков инициализации;
- никакого наследования.
Однако инлайн-классы могут:
- реализовать интерфейс;
- иметь свойства и функции.
Также стоит иметь в виду, что инлайн-классы не всегда будут развернуты в свой параметр. Главное правило здесь: объект инлайн-класса не будет развернут, если используется в качестве аргумента функции, ожидающей другой тип.
Например, функции для работы с коллекциями ( listOf(), setOf() и им подобные) обычно принимают на вход параметр типа Object или Any, так что переданный им объект инлайн-класса развернут не будет. Функция equals() также принимает в качестве аргумента тип Any, поэтому следующие два примера работают одинаково, но второй приведет к дополнительным накладным расходам:
1 2 3 4 5 6 7 8 |
val doggo1 = DogId(1L) val doggo2 = DogId(2L) // Оба объекта будут развернуты doggo1 == doggo2 // doggo1 будет развернут, а doggo2 — нет doggo1.equals(doggo2) |
Объект не будет развернут и если объект инлайн-класса передать функции, аргумент которой имеет nullable-тип:
1 2 3 4 5 6 |
val doggo = DogId(1L) fun pet(doggoId: DogId?) {} // Объект не будет развернут pet(doggo) |
Интересно также, что компилятор поддерживает переопределение функций, принимающих объект инлайн-класса и его необернутый аналог. То есть следующий код будет успешно скомпилирован:
1 2 |
fun pet(doggoId: Long) {} fun pet(doggoId: DogId) {} |
РЕКОМЕНДУЕМ:
Полезные советы разработчику на языке Kotlin
Ну и последнее, что стоит иметь в виду: инлайн-классы — это экспериментальная возможность, которая может измениться со временем или будет удалена.