СоХабр закрыт.
С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.
Эта статья посвящена описанию механизма генерации автоматических классов компилятором Kotlin
для анонимного блока пользовательского кода. Описано в каких случаях создаются автоматические классы, где они располагаются и как используются.
Статья основана на версии Kotlin
«1.1-М04».
В Java
существует только один способ создания элемента кода – это лямбда-функция, которая может реализовывать только функциональный интерфейс (подробнее см. в документации на Java
). В Kotlin
существует несколько различных способов описать объект, содержащий пользовательский код.
Всем хорошо известно и широко используется особенность Kotlin
очень легко реализовывать код, который позволяет писать такие конструкции:
fun Action( cb:()->Unit ) {}
fun Test() {
Action{
// Этот блок кода будет передан в функцию в качестве параметра
}
}
Как это реализовано и чем чревато использование такого кода в программе?
Описание отдельного кода в JVM
невозможно, во всех случаях компилятор автоматически создает классы, в которых будет выполняться указанный код. В разных случаях будут создаваться классы разного типа. Нюансы реализации этих классов и их вызова не играют особой роли в данном разделе, интересными являются только место создание класса и его размер.
Анонимная функция
Блок кода с произвольными параметрами и возвращаемым значением, который может быть вызван непосредственно или передан в качестве параметра в функцию.
fun Action( cb:()->Unit ) {}
fun statMethod() {}
fun Test() {
val cb = {}
cb()
Action( cb )
Action{}
Action( ::statMethod )
}
Анонимная функция объекта
Блок кода, который выполнится в контексте экземпляра объекта, т.е. будет иметь поле «this», ссылающееся на объект для которого вызван этот код.
fun cAction( cb:String.()->Unit ) {
val str = "text"
str.cb()
}
fun Test() {
cAction{
println( "Hello from anonymous receiver: $this" )
}
}
Лямбда-функция
Блок, реализуемый в автоматически создаваемом классе, наследнике функционального интерфейса, для единственного метода этого интерфейса.
class AA {
fun add(i : ActionListener) {
}
}
fun Test() {
val a = AA()
a.add(ActionListener { })
}
Анонимный класс
Автоматически создаваемый безымянный класс, с возможностью наследования от указанного класса.
class AA {
fun add(i : ActionListener) {
}
}
fun Test() {
val a = AA()
a.add(object : ActionListener { })
}
Давайте соберем воедино все описанные выше способы объявления автоматических объектов в один исходный файл и скомпилируем его.
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
interface Intf : ActionListener
class AA {
fun add(i : ActionListener) {}
}
fun cAction( cb:String.()->Int ) {
val str = "text"
str.cb()
}
fun Action( cb:()->Unit ) {}
fun statMethod() {}
fun Test() {
val a = AA()
//jm/test/ktest/KMainKt$Test$1
a.add(object : Intf {
override fun actionPerformed(e : ActionEvent?) {}
})
//jm/test/ktest/KMainKt$Test$2
a.add(ActionListener { })
//jm/test/ktest/KMainKt$Test$cb$1
val cb = {}
cb()
Action( cb )
//jm/test/ktest/KMainKt$Test$3
Action{}
//jm/test/ktest/KMainKt$Test$4
Action( ::statMethod )
//jm/test/ktest/KMainKt$Test$5
cAction( String::length )
}
Если посмотреть в каталог, куда компилятор сохранил созданные классы, то можно увидеть следующие файлы:
Имя файла | Размер, байт |
---|---|
AA.class | 888 |
Intf.class | 401 |
KMainKt.class | 2402 |
KMainKt$Test$1.class | 951 |
KMainKt$Test$2.class | 911 |
KMainKt$Test$3.class | 989 |
KMainKt$Test$4.class | 1413 |
KMainKt$Test$5.class | 1222 |
KMainKt$Test$cb$1.class | 995 |
Понятно откуда взялись первые три файла – это классы, которые описаны в нашей программе:
AA.class
» — это объявленный в тексте класс.Intf.class
» — это объявленный в тексте программы интерфейс.KMainKt.class
» — это основной класс программы Kotlin
, в который включены все данные и методы, не включенные в другие классы. Имя этого файла состоит из имени файла исходной программы (в нашем случае это «kMain.kt
») и суффикса «Kt
». В нашем примере в этот класс помещены методы «Action
», «cAction
», «statMethod
» и «Test
».Откуда взялись остальные файлы?
Эти файлы содержат в себе код классов, которые компилятор сгенерировал автоматически. В тексте программы приведенной выше, в комментариях, указаны имена сгенерированных классов, в соответствии месту их объявления в программе.
ВНИМАНИЕ: Компилятор создает каждый раз новый КЛАСС, а не новый экземпляр какого-то класса. Т.е. при каждом использовании кода в программе создается новый класс, который используется в единственном месте программы, создавая единственный его экземпляр!
Если посмотреть исходный текст, то можно обратить внимание на то, что текст, казалось-бы, не имеющий отношения к созданию временных объектов, так же приводит к генерации классов.
//jm/test/ktest/KMainKt$Test$4
Action( ::statMethod )
//jm/test/ktest/KMainKt$Test$5
cAction( String::length )
Это объясняется тем, что для обеспечения используемого в тексте программы синтаксиса компилятор просто создает классы обертки, вызывающие указанные методы. Описание в примере (за исключением незначимых нюансов) абсолютно эквивалентно коду с ручным вызовом.
Action{ statMethod() }
cAction{ length }
Если посмотреть на таблицу, приведенную выше, то можно заметить, что генерируемые файлы классов очень большие. Нужно учесть, что в рассматриваемом примере объем собственно полезного кода вообще нулевой — все тела блоков не содержат никаких операций, т.е. сгенерированный код состоит из единственной команды «return».
Если предположить, что в большинстве случаев код, который будет помещен в такой автоматический класс, будет довольно большим, то на объем этих файлов можно особого внимания не обращать, однако, такое предположение не соответствует действительности.
Как правило, объем кода размещаемого в автоматических блоках как раз очень невелик, и назвать создание файлов по 1-1.5Кб для каждого анонимного блока описанного в программе, эффективным или даже приемлемым довольно сложно.
Проведем эксперимент с нашим примером, изменив описание двух методов.
inline fun cAction( cb:String.()->Int ) {
val str = "text"
str.cb()
}
inline fun Action( cb:()->Unit ) {}
В этом случае мы добавили ключевое слово inline
к писанию функций.
После компиляции программы можно увидеть, что список файлов созданных компилятором для автоматических классов сократился до трех: «KMainKt$Test$1.class
», «KMainKt$Test$2.class
» и «KMainKt$Test$cb$1.class
». Т.е. остались только те классы, которые не передаются в качестве параметра функциям «Action» и «cAction». При этом размер файла «KMainKt.class
» увеличился, но всего на 180 байт!
Куда делись остальные классы?
Имея опыт использования классических языков программирования можно предположить, что ключевое слово inline
могло бы обозначать что-то типа: «подстановки кода в место использования». Т.е. то, что от создания лишних объектов компилятор избавился совсем, но это не так.
Классы для всех блоков кода, по прежнему, создаются в полном объеме, имеют точно те же имена и содержимое что и раньше, но они теперь помещены в файл того класса, где происходило их объявление, т.е., в нашем случае, в файл «KMainKt.class
».
Размер файла с классами, используемыми для inline
функций, увеличился всего на 180 байт, тогда как при их объявлении в виде отдельных файлов они, в сумме, занимали около 4Кб!
Проведем еще один эксперимент: проверим способности Kotlin
к оптимизации создания автоматических классов.
Если рассмотреть все места, где компилятор автоматически создает классы в нашем примере, то можно предположить наличие оптимизации связанной с тем, что классы будут создаваться только для блоков с уникальным кодом, а во всех остальных случаях будут использоваться объекты уже существующих классов.
Для проверки возможностей оптимизации просто размножим две строчки кода. Одну, использующую пользовательский код, и вторую, использующую полностью автоматический код.
Action{}
Action{}
Action{}
Action{}
Action( ::statMethod )
Action( ::statMethod )
Action( ::statMethod )
Action( ::statMethod )
В идеальном варианте Kotlin
оптимизирует использование автоматического класса как для первой группы, так и для второй. В хорошем варианте – только для второй. Проверка уникальности пользовательского кода может оказаться очень сложно реализуемой, поэтому будем рассчитывать только на элементарную оптимизацию классов, создаваемых компилятором полностью в автоматическом режиме.
Оставлю модификацию примера, его компиляцию и просмотр файлов классов в качестве самостоятельного упражнения и перейду сразу к результату.
Для каждого места описания кода будет создан новый уникальный класс!
К сожалению, в текущей версии Kotlin
(используется версия «1.1-М04») никакой оптимизации, связанной с генерацией автоматических классов не существует и при любом использовании кода будет создан новый автоматический класс.
Текущее поведение компилятора приводит к тому, что активное использование анонимного кода приводит к стремительному росту объема скомпилированной программы. Рост объема будет тем стремительнее, чем больше кода генерируется для использования вне функций с модификатором inline
.
В итоге описанных выше экспериментов на основе простейшей программы можно сделать следующие выводы:
Для каждого элемента кода, который описан в программе, будет создан отдельный класс, реализующий вызов этого кода.
Реализации автоматических классов будут размещены в отдельных файлах для всех применений, где экземпляр такого класса не передается сразу же в качестве параметра «inline» функции. Классы, передаваемые в «inline» функции будут созданы в полном объеме, но будут размещены в файле того класса, где происходило их описание. (Второй вариант приводит к генерации значительно меньшего объема данных.)
Детали реализации автоматических классов, используемой в Kotlin, не играют никакой роли и, для разных ситуаций они различны, но во всех случаях они довольно громоздки по объему и размещение их в раздельных файлах может привести к тому, что итоговый объем собранной программы, использующей такой код, будет расти очень быстро.
Kotlin создает отдельные и уникальные классы для каждого места использования кода, поэтому как-то сравнить их между собой невозможно.
Некоторые реализации автоматических классов используют заранее сконструированный уникальный объект для передачи его в качестве параметра, что оставляет надежду на будущую автоматизацию использования таких классов, но в настоящее время ее нет.
В текущей реализации нет абсолютно никакой разницы между использованием функций с модификатором «inline» и без него и, учитывая разницу в объеме итоговой программы, описание функций без этого модификатора не имеет никакого смысла. Возможно, что что-то изменится в будущих версиях компилятора, но пока везде где это возможно нужно использовать этот модификатор.
комментарии (13)