СоХабр закрыт.
С 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, не важны и, для разных ситуаций они различны, но они довольно громоздки (значительно больше аналогов, генерируемых Java) и размещение их в раздельных файлах может привести к тому, что итоговый объем собранной программы будет расти неадекватно быстро.
Kotlin создает отдельные и уникальные классы для каждого места использования кода, поэтому как-то сравнить их между собой невозможно. Правда типы таких классов доступны, но их описание и использование выходит за рамки этой статьи.
Некоторые реализации автоматических классов используют заранее сконструированный уникальный объект для передачи его в качестве параметра, что оставляет надежду на будущую автоматизацию использования таких классов, но в настоящее время ее нет.
В текущей реализации нет абсолютно никакой разницы между использованием функций с модификатором «inline» и без него и, учитывая разницу в объеме итоговой программы, описание функций без этого модификатора не имеет никакого смысла. Возможно, что что-то изменится в будущих версиях компилятора, но пока везде где это возможно нужно использовать этот модификатор.
комментарии (13)