group-telegram.com/rareilly/164
Last Update:
Продолжаем тренировать #насмотренность с inline-классами. В предыдущем посте я обещал рассказать про boxing inline-классов. Но сначала немного поговорим про примитивы.
Один из моих любимых вопросов на собеседовании — есть ли в 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
.BY Ra'Reilly - Заметки про Android и не только
Warning: Undefined variable $i in /var/www/group-telegram/post.php on line 260
Share with your friend now:
group-telegram.com/rareilly/164