Kotlin

Kotlin 1.4.30: новые возможности языка

Read this post in other languages:
English, Français, 한국어, Deutsch, Português do Brasil, Español, 简体中文

Согласно новому графику релизов Kotlin 1.5 выйдет только через несколько месяцев, но попробовать новые возможности языка можно уже сейчас — в версии 1.4.30:

Чтобы попробовать новые функции, нужно указать версию языка 1.5.

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

Стабилизация inline-классов значений

В альфа-режиме inline-классы были представлены еще в Kotlin 1.3, а в версии 1.4.30 они перешагнули на стадию беты.

В Kotlin 1.5 inline-классы станут стабильными и будут относиться к более широкому понятию — классам значений, о которых мы рассказываем ниже.

Для начала вспомним, как работают inline-классы. Если вы с ними уже знакомы, смело переходите к следующему разделу.

Напоминаем, что inline-класс ликвидирует обертку вокруг значения:

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

Компилятор заменяет экземпляры inline-класса (в нашем примере экземпляр Color) соответствующим типом (Int) в байт-коде, если это возможно:

При этом компилятор генерирует функцию changeBackground с искаженным именем, принимающую Int в качестве параметра, и передает константу 255 напрямую, без создания обертки в месте вызова:

Имя искажено, чтобы обеспечить перегрузку функций, принимающих экземпляры различных inline-классов, и предотвратить случайные вызовы из Java-кода, которые могут нарушить внутренние ограничения inline-класса. Читайте далее, как сделать, чтобы это работало из Java.

Обертка не всегда удаляется в байт-коде. Это происходит, только если есть возможность, и очень похоже на примитивные типы. Когда вы определяете переменную типа Color или передаете ее в функцию напрямую, она заменяется соответствующим значением:

В этом примере переменная color во время компиляции имеет тип Color, а в байт-коде заменена на Int.

Однако если вы сохраните ее в коллекции или передадите в обобщенную функцию, она будет упакована в обычный объект типа Color:

Упаковка и распаковка выполняются компилятором автоматически. Вам не нужно ничего с этим делать, но полезно знать, как это работает.

Изменение JVM-имен для вызовов из Java

Начиная с версии 1.4.30, вы можете изменить JVM-имя функции, принимающей inline-класс в качестве параметра, чтобы ее можно было вызывать из Java. По умолчанию такие имена иcкажаются, чтобы предотвратить случайное использование Java и конфликтующие перегрузки (например, changeBackground-euwHqFQ в примере выше).

Аннотация @JvmName изменяет имя функции в байт-коде и позволяет вызывать ее из Java и напрямую передать значение:

Как и любую функцию с аннотацией @JvmName, из Kotlin вы вызываете ее по имени Kotlin. Использование в Kotlin типобезопасно, поскольку в качестве аргумента можно передать только значение типа Timeout, а единицы измерения очевидны.

Из Java можно напрямую передать значение long. Это уже не будет типобезопасно и поэтому не работает по умолчанию. Если вы видите в коде greetAfterTimeout(2), не сразу понятно, что имеется в виду: 2 секунды, 2 миллисекунды или 2 года.

Добавляя аннотацию, вы подчеркиваете, что собираетесь вызывать эту функцию из Java. Смысловое имя помогает избежать путаницы: добавление суффикса «Millis» делает единицы понятными для пользователей Java.

Блоки init

Еще одно улучшение inline-классов в 1.4.30 заключается в том, что теперь можно задавать логику инициализации в блоке init:

Раньше так было делать нельзя.

Подробнее об inline-классах читайте в соответствующем KEEP, в документации и в комментариях к этой задаче.

Inline-классы значений

В Kotlin 1.5 inline-классы станут стабильными и будут относиться к более широкому понятию — классам значений.

До сих пор inline-классы были отдельной языковой конструкцией, а теперь представляют собой специальную JVM-оптимизацию класса значений с одним параметром. Классы значений — более широкий концепт, и в дальнейшем они будут поддерживать различные оптимизации. Сейчас они поддерживают inline-классы, а впоследствии будут поддерживать примитивные классы Valhalla, когда проект Valhalla будет завершен (подробности далее).

Единственное, что меняется сейчас, — это синтаксис. Поскольку inline-класс является оптимизированным классом значений, его следует объявлять по-новому:

Вы определяете класс значений с одним параметром конструктора и добавляете аннотацию @JvmInline. Мы ожидаем, что с выходом Kotlin 1.5 все будут использовать новый синтаксис. Старый синтаксис inline class будет работать еще некоторое время, но будет считаться устаревшим. В версии 1.5 появится предупреждение с предложением миграции. А впоследствии старый синтаксис будет считаться ошибочным.

Классы значений

Класс value представляет собой неизменяемую сущность с данными. На данный момент такой класс может содержать только одно свойство для поддержки использования «старых» inline-классов.

В будущих версиях Kotlin можно будет определять классы значений с несколькими свойствами. Все значения должны быть неизменяемыми val:

Классы значений не имеют идентичности: они полностью определяются хранящимися данными, и проверки === для них не разрешены. Проверка на равенство == автоматически сравнивает содержащиеся данные.

Такое отсутствие идентичности дает много возможностей для дальнейших оптимизаций: проекта Valhalla позволит реализовывать классы значений как примитивные JVM-классы.

Ограничение неизменности и, следовательно, возможность оптимизации отличает классы значений от data-классов.

Оптимизации из проекта Valhalla

Проект Valhalla предлагает новый концепт для Java и JVM — примитивные классы.

Основная цель примитивных классов — добавить к производительности примитивов объектно-ориентированные преимущества обычных JVM-классов. Примитивные классы — это носители данных, экземпляры которых могут храниться в переменных и в стеке вычислений. Работать с ними можно напрямую, без заголовков и указателей. В этом отношении они похожи на примитивные значения, такие как int, long и т. д. (в Kotlin вы не работаете с примитивными типами напрямую, но компилятор генерирует их под капотом).

Важным преимуществом примитивных классов является то, что они позволяют плоскую и плотную компоновку объектов в памяти. Сейчас Array<Point> представляет собой массив ссылок. Проект Valhalla подразумевает, что при определении Point как примитивного класса (в терминологии Java) или как класса значений с соответствующей оптимизацией (в терминологии Kotlin) JVM может оптимизировать его и хранить массив Point в «плоской» компоновке, как массив из множества x и y, а не как массив ссылок.

Мы с нетерпением ждем предстоящих изменений в JVM и хотим, чтобы Kotlin извлек из них пользу. В то же время мы не хотели бы заставлять вас использовать только новые версии JVM для работы с value classes, поэтому собираемся поддерживать и более ранние версий JVM. При компиляции кода в JVM с поддержкой Valhalla последние JVM-оптимизации будут работать для классов значений.

Изменяющие методы

О функциональности классов значений можно сказать гораздо больше. Поскольку классы значений представляют собой «неизменяемые» данные, для них возможны изменяющие методы, подобные тем, что есть в Swift: функция-член или сеттер свойства возвращает новый экземпляр, а не обновляет существующий. Главное преимущество — вы используете их со знакомым синтаксисом. Все это еще предстоит прототипировать в языке.

Где читать подробнее

Аннотация @JvmInline предназначена для JVM. На других бэкендах классы значений могут быть реализованы иначе, например как структуры Swift в Kotlin/Native.

Читайте подробнее о классах значений в этом документе или смотрите отрывок из выступления Романа Елизарова.

Поддержка JVM-записей

Еще одно предстоящее улучшение экосистемы JVM — это Java records (записи). Они аналогичны data-классам в Kotlin и в основном используются как простые хранилища данных.

Записи Java не придерживаются соглашения JavaBeans, и в них используются методы «x()» и «y()» вместо привычных «getX()» и «getY()».

Совместимость с Java всегда была для нас в приоритете. Kotlin-код «понимает» новый синтаксис и рассматривает записи как классы со свойствами Kotlin. Работает так же, как и для обычных Java-классов, соответствующих JavaBeans:

В основном из-за совместимости нельзя аннотировать класс data с помощью @JvmRecord, чтобы сгенерировать новые методы JVM-записи:

Аннотация @JvmRecord заставляет компилятор генерировать методы x() и y() вместо стандартных getX() и getY(). Предполагаем, что эта аннотация нужна вам только для сохранения API класса при его преобразовании из Java в Kotlin. Во всех остальных случаях можно без проблем использовать привычные data-классы Kotlin.

Аннотация доступна, только если вы компилируете Kotlin-код под JVM версии 15+. Подробнее об этой функции читайте в соответствующем KEEP, в документации и в комментариях к этой задаче.

Sealed-интерфейсы и улучшения sealed-классов

Когда класс изолирован (объявлен как sealed), иерархия ограничивается определенными подклассами — это позволяет проверить, что ветви when охватывают все случаи. В Kotlin 1.4 иерархия sealed-классов имеет два ограничения. Во-первых, вышестоящий класс не может быть sealed-интерфейсом — он должен быть классом. Во-вторых, все подклассы должны находиться в одном файле.

В Kotlin 1.5 этих ограничений нет: изолированным можно сделать и интерфейс. Подклассы (как sealed-классов, так и sealed-интерфейсов) должны относиться к той же единице компиляции и к тому же пакету, что и родительский класс, но теперь они могут находиться в разных файлах.

Sealed-классы, а теперь и интерфейсы, полезны при создании иерархий абстрактных типов данных (ADT).

Еще один важный вариант использования sealed-интерфейсов — это ограничение наследования и реализации за пределами библиотеки. Интерфейс, определенный как sealed, может быть реализован только в той же единице компиляции и в том же пакете, что делает невозможным реализацию вне библиотеки.

Например, подразумевается, что интерфейс Job из пакета kotlinx.coroutines может быть реализован только внутри библиотеки kotlinx.coroutines. Если сделать интерфейс изолированным, это будет выражено явно:

Как пользователю библиотеки вам больше нельзя определять собственный подкласс Job. Это всегда «подразумевалось», но с sealed-интерфейсами может быть формально запрещено компилятором.

Использование JVM-поддержки в будущем

Предварительная поддержка sealed-классов была представлена ​​в Java 15 и JVM. В дальнейшем мы собираемся использовать стандартную JVM-поддержку sealed-классов при компиляции Kotlin-кода под JVM последней версии (скорее всего, это будет JVM 17 или более поздняя версия — когда эта функция станет стабильной).

В Java вы явно перечисляете все подклассы данного изолированного класса или интерфейса:

Эта информация сохраняется в файле класса с помощью нового атрибута PermittedSubclasses. JVM распознает изолированные классы во время выполнения и не дает расширять их неразрешенными подклассами.

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

А в Kotlin список подклассов указывать не нужно. Компилятор сгенерирует список на основе объявленных подклассов в том же пакете.

Есть шанс, что возможность явно указывать подклассы для родительского класса будет добавлена ​​позже в качестве дополнительной спецификации. Пока что мы не видим в этом необходимости, но будем рады услышать от вас, нужна ли вам такая функция.

Обратите внимание, что более старые версии JVM в теории позволяют определить Java-подкласс для изолированного интерфейса Kotlin, но лучше этого не делать. Поскольку поддержка разрешенных подклассов в JVM еще не доступна, ограничение применяется только компилятором Kotlin. Мы добавим предупреждения в IDE, чтобы этого не происходило случайно. В будущем новый механизм будет работать для новейших версий JVM, чтобы гарантировать отсутствие «неразрешенных» подклассов из Java.

Подробнее об изолированных интерфейсах и ослабленных ограничениях для изолированных классов читайте в соответствующем разделе KEEP, в документации и в комментариях к этой задаче.

Как все это попробовать

Чтобы попробовать новые возможности, используйте Kotlin 1.4.30 и укажите версию языка 1.5:

Чтобы попробовать JVM-записи, нужно также использовать jvmTarget 15 и включить JVM-превью: добавьте параметры компилятора -language-version 1.5 и -Xjvm-enable-preview.

Примечания к предрелизной версии

Обратите внимание, что поддержка новых функций является экспериментальной, а поддержка версии 1.5 считается предварительной. Выбрав версию 1.5 в компиляторе Kotlin 1.4.30, вы будете как будто использовать превью-версию 1.5-M0. Гарантии обратной совместимости не распространяются на предварительные версии. Последующие релизы могут вносить изменения в функциональность и API. После выхода версии Kotlin 1.5-RC все бинарные файлы, созданные предрелизными версиями, будут запрещены в компиляторе: вам потребуется перекомпилировать все, что было скомпилировано в версиях 1.5-Mx.

Что думаете?

Пробуйте новые функции и делитесь своими впечатлениями! Подробнее о нововведениях читайте в соответствующих разделах KEEP и присоединяйтесь к обсуждениям в YouTrack. А если у вас что-то не работает, обязательно сообщите нам. Мы будем ждать отзывов о том, как новые возможности языка реализуются в ваших проектах.

Что еще почитать:

Ваша команда Kotlin
The Drive to Develop

Discover more