Сегодня поговорим о 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"); // Теперь все ок } }
Ограничения инлайн-классов
Не все так радужно. Есть некоторые ограничения:
-
Наследование: инлайн-классы не могут наследоваться и не могут быть родителями других классов.
// Ошибка компиляции! value class MyInt(val value: Int) : Number()
-
Свойства только val: внутри инлайн-класса все свойства должны быть
val
.// Ошибка компиляции! value class MutableValue(var value: Int)
-
Инициализатор: нельзя иметь дополнительные свойства или инициализацию вне главного конструктора.
// Ошибка компиляции! 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/
Добавить комментарий