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

21b28b7cb0e8d4adec22fb872cb76f69.png

Сегодня поговорим о 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(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 упрощает код.

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

© Habrahabr.ru