Классы без лишнего веса: инлайн-классы в Kotlin

от автора

Сегодня поговорим о Kotlin и его инлайн‑классах. Честно говоря, когда я впервые услышал об этой фиче, подумал: «Опять что‑то выдумали, чтобы жизнь медом не казалась». Но, разобравшись, понял, что это очень даже полезная штука.

Зачем нам инлайн-классы?

Начнем с простого. Представьте, что у вас есть идентификаторы пользователей, заказов или каких-нибудь транзакций. Все это, как правило, обычные числа или строки. Но вот беда: легко перепутать userId с orderId, ведь оба — Int или String. Ошибки из-за этого могут быть самыми неприятными.

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

Создаем первый инлайн-класс

Посмотрим, как это выглядит на практике.

@JvmInline value class UserId(val id: Int)

Обратите внимание на аннотацию @JvmInline и ключевое слово value. Это говорит компилятору, что мы хотим создать инлайн-класс. Теперь мы можем использовать UserId как отдельный тип:

fun getUserName(userId: UserId): String {     // какая-то логика получения имени пользователя     return "User_${userId.id}" }  val userId = UserId(42) println(getUserName(userId)) // Output: User_42

Теперь, если вы случайно попытаетесь передать OrderId вместо UserId, компилятор вас остановит:

@JvmInline value class OrderId(val id: Int)  val orderId = OrderId(100)  // Ошибка компиляции! // println(getUserName(orderId))

Как это работает?

Инлайн-классы компилируются таким образом, что их экземпляры инлайнятся в байт-коде. Это означает, что при использовании UserId фактически передается просто Int, без дополнительных объектов.

Например, функция:

fun processUserId(userId: UserId) {     println("Processing user ID: ${userId.id}") }  val userId = UserId(123) processUserId(userId) 

Компилируется в байт-код, эквивалентный:

fun process(id: Int) {     println(id) }

Таким образом, мы получаем типобезопасность на уровне исходного кода и отсутствие накладных расходов на уровне выполнения.

Добавляем методы и свойства

Инлайн-классы могут содержать методы и вычисляемые свойства:

@JvmInline value class Email(val address: String) {     val domain: String         get() = address.substringAfter('@')      fun isValid(): Boolean {         return address.contains("@") && address.contains(".")     } }  val email = Email("test@example.com") println(email.domain) // Output: example.com println(email.isValid()) // Output: true

Можно даже перегружать операторы:

@JvmInline value class Dollars(val amount: Int) {     operator fun plus(other: Dollars) = Dollars(this.amount + other.amount) }  val wallet1 = Dollars(50) val wallet2 = Dollars(70) val total = wallet1 + wallet2 println(total.amount) // Output: 120 

Инлайн-классы прекрасно работают с коллекциями:

val userIds = listOf(UserId(1), UserId(2), UserId(3)) userIds.forEach { println(it.id) }

И даже с обобщениями:

fun  getFirstElement(list: List): T {     return list.first() }  val firstUserId = getFirstElement(userIds) println(firstUserId.id) // Output: 1

Взаимодействие с Java

Если вы используете Kotlin вместе с Java, будьте внимательны. Инлайн-классы в Kotlin выглядят как их базовые типы в Java:

// Kotlin @JvmInline value class Token(val value: String)  // Java public class Main {     public static void main(String[] args) {         Token token = new Token("abc123"); // Ошибка компиляции!     } }

Чтобы Java могла использовать ваш инлайн-класс, нужно предоставить вспомогательный метод-фабрику:

// Kotlin @JvmInline value class Token(val value: String) {     companion object {         @JvmStatic         fun create(value: String) = Token(value)     } }  // Java public class Main {     public static void main(String[] args) {         Token token = Token.create("abc123"); // Теперь все ок     } }

Ограничения инлайн-классов

Не все так радужно. Есть некоторые ограничения:

  1. Наследование: инлайн-классы не могут наследоваться и не могут быть родителями других классов.

    // Ошибка компиляции! value class MyInt(val value: Int) : Number()
  2. Свойства только val: внутри инлайн-класса все свойства должны быть val.

    // Ошибка компиляции! value class MutableValue(var value: Int)
  3. Инициализатор: нельзя иметь дополнительные свойства или инициализацию вне главного конструктора.

    // Ошибка компиляции! value class Invalid(val x: Int) {     val y = x * 2 }

Использование с nullable типами

Инлайн-классы могут быть nullable:

val nullableUserId: UserId? = null  fun printUserId(userId: UserId?) {     if (userId != null) {         println("User ID: ${userId.id}")     } else {         println("User ID is null")     } }  printUserId(nullableUserId) // Output: User ID is null

Но есть нюанс: nullable инлайн-классы не инлайнятся и представляют собой полноценные объекты.

Инлайн-классы и сериализация

С библиотекой kotlinx.serialization можно работать с инлайн-классами:

@Serializable @JvmInline value class ProductCode(val code: String)  val productCode = ProductCode("ABC123") val json = Json.encodeToString(productCode) println(json) // Output: "ABC123"  val decoded = Json.decodeFromString<ProductCode>(json) println(decoded.code) // Output: ABC123

Рефлексия

С рефлексией все немного сложнее. Инлайн-классы могут вести себя неожиданно при использовании рефлексии:

@JvmInline value class Tag(val value: String)  fun main() {     val tag = Tag("Kotlin")     val kClass = tag::class     println(kClass.simpleName) // Output: String, а не Tag! }

Инлайн-классы против типалиасов

Можно задаться вопросом: а почему бы не использовать typealias?

@Serializable @JvmInline value class ProductCode(val code: String)  val productCode = ProductCode("ABC123") val json = Json.encodeToString(productCode) println(json) // Output: "ABC123"  val decoded = Json.decodeFromString(json) println(decoded.code) // Output: ABC123

Но typealias не создают новый тип, это просто псевдоним. Компилятор не поможет, если вы перепутаете UserId и OrderId. Инлайн-классы же создают новый тип с полной типобезопасностью.


Если остались вопросы или хотите поделиться своим опытом — пишите, обсудим!

А завтра вечером (12 ноября) в Otus пройдет открытый урок, на котором участники рассмотрят ключевые отличия между Kotlin и Java.

Первая часть занятия будет посвящена таким концепциями, как null-безопасность, сокращение шаблонного кода, лямбда-выражения, и другим преимуществам Kotlin. Во второй части участники напишут веб-сервис с CRUD операциями на Java, а затем преобразуют его в Kotlin, чтобы на практике увидеть, как синтаксис Kotlin упрощает код.

Если заинтересовало, записывайтесь на занятие по ссылке.


ссылка на оригинал статьи https://habr.com/ru/articles/855160/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *