Встроенные классы в Kotlin (Inline Classes)

 

Kotlin — язык программирования с открытым кодом, который с каждым днём становится всё более популярным среди разработчиков Java. По сравнению с Java он лаконичнее, современнее и выразительнее, работая с Kotlin, вы получаете более богатый опыт разработки. При создании мобильных приложений для Android его можно использовать вместо или вместе с Java. Но всё ли в Kotlin так хорошо и удобно?

 

В этой статье мы расскажем об одной из относительно новых функций языка Kotlin — встроенных классах (Value). Впервые о них заговорили, когда программисты работали в Kotlin 1.3. Встроенные классы Kotlin должны были стать типобезопасной альтернативой typealias, поддерживаемой компилятором без дополнительных накладных расходов.

 

В Kotlin 1.4.30 встроенные классы/классы значений стали доступны в режиме предварительного просмотра. Стабильная версия встроенных классов появилась в версии Kotlin 1.5.

Что такое встроенный класс

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

 

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

 

На практике это означает, что вам может потребоваться включить значение в класс, чтобы убедиться, что вы не перепутали его по ошибке с другой переменной аналогичного типа. К сожалению, это случается чаще, чем хотелось бы и нередко причину проблемы найти довольно трудно.

 

Чтобы всё было предельно точно, можно создать оболочку UserId, которая используется вместо Int, это позволит избежать ошибок, т.к. смешивать аргументы неправильно будет невозможно.

 

Создание этого класса-оболочки добавляет в код ещё один класс, увеличивая количество классов и снижая производительность. Чтобы этого избежать, можно использовать встроенный класс.

 

Несмотря на то, что вы не будете выделять новый класс, компилятор будет действовать так, как будто вы это сделали.

Совместимость для разработчиков Android / Kotlin

Встроенный класс на практике

Сравнивать встроенные классы можно только с другими классами того же типа:

val userId1 = UserId(1)
val userId2 = UserId(2)
assert(userId1 == userId2)
assert(userId1 == 2) <-- Does not compile

 

Сравнивать встроенные классы с помощью ссылок нельзя:

assert(userId1 === userId2) <-- Does not compile

 

Встроенные классы являются окончательными и не могут быть расширены:
class OwnerId(id: Int): UserId(id) <-- Does not compile

У встроенных классов может быть только один основной параметр конструктора:
@JvmInline
value class User(val id: Int, val name: String) <-- Does not compile

 

Встроенные классы могут переносить сложные классы:

class User(val id: Int, val name: String)
@JvmInline
value class Staff(val user: User)

 

Также встроенные классы могут переносить другие встроенные классы (даже если вы пока не знаете, зачем вам это нужно):

@JvmInline
value class CoolerUserId(val id: UserId)

 

Встроенные классы не могут быть объявлены как const (что довольно удивительно):

const val id = 1
const val userId = UserId(1) <-- Does not compile

 

Встроенные классы toString () напоминают toString () реализацию класса данных:

println(userId1) --> "UserId(id=1)"

 

Во встроенных классах можно переопределять toString (), но не equals () и hashCode (), поскольку сейчас они зарезервирован в Kotlin для использования в будущем:

@JvmInline
value class UserId(val id: Int) {
override fun toString(): String {...}
override fun equals(o: Any?): Boolean {...} <-- Does not compile
override fun hashCode(): Int {...} <-- Does not compile
}

 

Встроенные классы теперь поддерживают init конструктор:

@JvmInline
value class UserId(val id: Int) {
init {
require(id > 0) <- Runtime validation
}
}

 

У встроенных классов не может быть резервных полей:

@JvmInline
value class UserId(val id: Int) {
lateinit var isValid: Boolean <-- Does not compile
val negativeId = -id <-- Does not compile
}

 

Встроенный класс может объявлять функции и свойства:

@JvmInline
value class UserId(val id: Int) {
fun isValid(): Boolean = id > 0
val negativeId: Int
get() = -id
}

 

Поскольку вы можете сравнивать встроенный класс только с другим классом того же типа, вам придется деструктировать свой класс, чтобы сравнить его с любым базовым типом:

assert(userId1.id == 1)
assert(userId1 == UserId(1))

 

Не все воспринимают это как существенный недостаток, но это может раздражать при переносе кода во встроенный класс вместо простого String или Int.

 

Классы данных

Класс данных, обертывающий класс значений, может использовать свои equals, to String, hash Code и copy функции.

 

Переоборудование

Встроенные классы открывают множество возможностей, среди них создание более безопасных служб REST:

data class UserResponse(val id: UserId, val name...)

interface UserService {
@GET("/users/{userId}")
suspend fun getUser(@Path("userId") userId: UserId): UserResponse
@GET("/users/{userId}/id")
suspend fun getUserId(@Path("userId") userId: UserId): UserId
}

 

Parcelable / Parcelize

Встроенные классы не работают с @Parcelize / Parcelable:

@JvmInline
@Parcelize
value class UserId(val id: Int) : Parcelable <-- Does not compile

 

Также встроенные классы не работают, вложенные внутри Parcelable:

@JvmInline
@Parcelize
data class User(val id: UserId) : Parcelable

 

Будет бросать RuntimeException при попытке сериализовать/десериализовать его:

Parcel: unable to marshal value

 

Мы надеемся, что @Parcelize будет исправлен и доработан в ближайшее время, поскольку сейчас это больше похоже на демонстрацию для разработчиков Android того, как это всё может выглядеть.

Вывод

Стоит ли использовать встроенные классы?

Будем предельно честными: на данном этапе продукт сырой и неудобный. При работе возникает слишком много проблем, а выгоды не так уж и много, чтобы рассматривать встроенные классы Kotlin более серьёзно.

 

Альтернативы

Если вы хорошо пишете тесты и у вас хорошие экспертные оценки кода, возможно, вам стоит придерживаться простого Int, Long or String. Но, поскольку это чревато сложными ошибками, вы можете использовать подход класса Wrapper, который должны были решать встроенные классы.

 

Мы возлагали большие надежды на стабильную версию и нам жаль, что они не оправдались. Надеемся, что обнаруженные проблемы будут решены в ближайшем будущем. Или они уже решены, а мы просто недостаточно хорошо искали решение.

 

В любом случае, прежде чем перейти на встроенные классы, тщательно взвесьте все «за» и «против». Возможно, вам они не покажутся такими проблемными.

Вам нужна наша помощь?

Pomegranate Square

Читайте также