Один из моих любимых вопросов на собеседовании — есть ли в Kotlin примитивы? Вот в Java есть примитив
int и объект Integer, а в Kotlin только Int. На самом деле при компиляции под JVM компилятор сам решает что использовать. Когда возможно, использует примитив, но если тип используется в дженерике или объявлен как nullable, нужен объект и происходит boxing — примитив оборачивается в объект Integer.Теперь вернёмся к inline-классам. Подобно примитивам, при работе с inline-классами Kotlin старается избежать лишних обёрток. Чтобы убедиться, что в конкретном случае не происходит boxing, можно посмотреть как код выглядит для JVM через
Show Kotlin Bytecode > Decompile. Случаи, когда происходит boxing, подробно описаны в KEEP. Рекомендую прочитать полностью, а пока сосредоточимся на самых интересных моментах.0️⃣ Нуллабельность
Если нужна нуллабельность, но хочется обойтись без boxing'а, можно создать специальное значение, которое будет заменять
null. Помните Color.Unspecified в Compose? Это как раз оно. Кстати, недавно подобные специальные значения добавили и inline-классам входящим в состав TextStyle. В сообщении к коммиту есть бенчмарки показывающие сколько аллокаций удалось на этом сэкономить.1️⃣ Создание подтипов
Когда в Kotlin появились sealed-интерфейсы, я подумал что они будут неплохо комбинироваться с inline-классами. Была мысль завести абстракцию чтобы одинаково работать со строками и текстовыми ресурсами:
sealed interface TextValue {
fun get(resources: Respurces): String
@JvmInline
value class Plain(val value: String) : TextValue {
fun get(resources: Resources) = value
}
@JvmInline
value class Res(@StringRes val resId: Int) : TextValue {
fun get(resources: Resources) = resources.getString(resId)
}
}Проблема в том, что тут от inline-классов нет никакого толка. Мы будем использовать интерфейс, а не конкретный inline-класс, так что boxing будет происходить всегда. Компилятор не знает какую именно реализацию интерфейса мы подложим, а значит не может заменить интерфейс на внутренний тип.
Вариантов минимизации boxing'а в подобных случаях как минимум два:
☝️ Если тип внутреннего значения у разных "подтипов" разный, можно хранить общий супертип. Так сделано в Result, внутри лежит
Any?, а чтобы отличать контент от ошибки, создан вспомогательный тип Failure, в который оборачиваются ошибки.✌️ Можно комбинировать тип и значение. Это хорошо работает с примитивами. Например, в Compose
TextUnit может иметь размерность Sp и Em, но это всё один inline-класс. Информация о типе хранится в младших битах Long'а, а значение в следующих за ними битах. Другой пример такого подхода — Duration из stdlib.2️⃣ Дженерики
С использованием inline-класса на позиции дженерика всё понятно — в этом случае без boxing'а не обойтись. Но дженерики указанные у самого inline-класса не влияют на boxing.
Классный пример применения inline-классов с дженериками — безопасные ID. Представим, что у нас есть несколько разных сущностей со строковыми ID и мы хотим чтобы на уровне контракта нельзя было по ID пользователя запросить товар и наоборот. В 2019 году Jake Wharton предложил создавать inline-классы для строгой типизации ID. С тех пор появилась возможность указывать дженерики у inline-классов и теперь можно не плодить отдельные классы-обёртки на каждый тип сущности, достаточно создать один inline-класс с дженериком:
@JvmInline
value class Id<out T>(val value: String)
data class User(val id: Id<User>)
Всем inline-классы в код!
UPD: @senk0n подсказывает, что Romain Guy недавно написал пример как можно в inline-класс запихнуть целую сетку 8х8, каждая ячейка которой может иметь значение
1 или 0.