Автоматические объекты компилятора

    Статья основана на версии Kotlin «1.1-М04».


    Эта статья посвящена описанию механизма генерации автоматических классов компилятором Kotlin для анонимного блока пользовательского кода. Описано в каких случаях создаются автоматические классы, где они располагаются, как реализованы и используются.


    В Java существует только один способ создания элемента кода – это лямбда-выражение, которое может реализовывать только функциональный интерфейс (подробнее см. в документации на Java). В Kotlin существует несколько различных способов описать объект, содержащий пользовательский код.


    Всем хорошо известна особенность Kotlin очень легко реализовывать код, позволяющий писать такие конструкции:


    fun Action( cb:()->Unit ) { cb() }
    fun Test() {
      Action{
        // Этот блок кода будет выполнен в функции
      }
    }

    Как это реализовано и чем чревато использование такого кода в программе?


    ПС: Первоначальный вариант статьи содержал логическую ошибку и неправильные выводы. Приношу всем свои извинения за этот факт. Эта статья переработана и дополнена дополнительными данными.



    Автоматические объекты компилятора


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


    Kotlin позволяет реализовать конструкции с анонимным кодом с использованием различного синтаксиса. Примеры ниже разделены по их способу использования в тесте программ, а не по способу реализации их компилятором.
    Фуктически, практически все перечисленные синтаксические конструкции реализованы одними и тем же способом.


    Анонимная функция


    Блок кода с произвольными параметрами и возвращаемым значением, который может быть вызван непосредственно или передан в качестве параметра в функцию.


    Пример анонимной функции
    fun Action(cb : () -> Int) //Тестовая функция
      = println(cb())
    
    class BB {
      fun F() = 1133
      fun M() {
        Action(this::F) //Создание блока для вызова функции
      }
    }
    
    fun statMethod() = 1111
    
    fun Test() {
      val cb = { 1122 }    // Создание объекта для кода
      cb()                 // Вызов кода для созданного объекта
      Action(cb)           // Передача блока в функцию
      Action { 1133 }      // Создание блока и передача его в функцию
      Action(::statMethod) // Создание блока для вызова функции
    }

    Конструкци "::statMethod" и "this::F" используются для облегчения синтаксиса и полностью эквивалентны ручному вызову функции в анонимном коде:


      Action{ statMethod() }  // аналог Action(::statMethod)
      Action{ F() }           // аналог Action(this::F)

    Анонимная функция объекта


    Блок кода, который выполнится в контексте экземпляра объекта, т.е. будет иметь поле «this», ссылающееся на объект для которого вызван этот код.


    В реализации этого подхода в Kotlin объект this эмулируется для упрощения синтаксиса, но фактически используется неявно переданный экземпляр объекта. Стоит отметить, что несмотря на наличие ключевого слова this и обращение к элементам класса без указания явного объекта, выполняемый код не является частью класса и, соответственно, не имеет доступа к его защищенным элементам.


    Пример анонимной функции объекта
    import java.awt.event.ActionEvent
    import java.awt.event.ActionListener
    
    fun <T> cAction(v : T, cb : T.() -> Int) // Тестовая функция для вызова метода объекта
      = println(v.cb())
    class BB {
      fun F() = 1133  // Функция класса
      fun M() {
        cAction(this, BB::F) // Создание блока для вызова функции объекта 
      }
    }
    fun Test() {
      cAction("text", String::length) // Создание блока для вызова функции объекта
    }

    Лямбда-функция


    Блок, реализуемый в автоматически создаваемом классе, наследнике функционального интерфейса, для единственного метода этого интерфейса.


    Пример лямбда-функции
    fun add(i : ActionListener) {} // Использование интерфейса
    fun Test() {
      add(ActionListener { })      // Создание объекта, реализующего интерфейс
    }

    Анонимный класс


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


    Пример анонимного класса
    fun add(i : ActionListener) {} // Использование интерфейса
    fun Test() {
      add(object : ActionListener {// Создание объекта, реализующего интерфейс
        override fun actionPerformed(e : ActionEvent?) {}
      })
    }


    Внешние автоматические классы


    Давайте соберем воедино все описанные выше способы объявления автоматических объектов в один исходный файл и скомпилируем его.


    Полный текст тестового кода
    import java.awt.event.ActionEvent
    import java.awt.event.ActionListener
    
    fun add(i : ActionListener) {}
    
    fun <T> cAction(v : T, cb : T.() -> Int) = println(v.cb())
    fun Action(cb : () -> Int) = println(cb())
    
    fun statMethod() = 1111
    
    class BB {
      fun F() = 1133
    
      fun M() {
        //jm/test/ktest/BB$M$1.INSTANCE
        cAction(this, BB::F)
    
        //jm/test/ktest/BB$M$2
        Action(this::F)
    
        //jm/test/ktest/BB$M$3
        Action{F()}
      }
    }
    
    fun Test() {
      //jm/test/ktest/KMainKt$Test$1
      add(object : ActionListener {
        override fun actionPerformed(e : ActionEvent?) {}
      })
    
      //jm/test/ktest/KMainKt$Test$2.INSTANCE
      add(ActionListener { e-> })
    
      //jm/test/ktest/KMainKt$Test$cb$1.INSTANCE
      val cb = { 1122 }
      cb()
      Action(cb)
    
      //jm/test/ktest/KMainKt$Test$3.INSTANCE
      Action { 1133 }
    
      //jm/test/ktest/KMainKt$Test$4.INSTANCE
      Action(::statMethod)
    
      //jm/test/ktest/KMainKt$Test$5.INSTANCE
      cAction("text", String::length)
    }

    Если посмотреть в каталог, куда компилятор сохранил созданные классы, то можно увидеть следующие файлы:


    Имя файла Размер, байт
    BB.class 1150
    KMainKt.class 3169
    BB$M$1.class 1710
    BB$M$2.class 1379
    BB$M$3.class 1025
    KMainKt$Test$1.class 973
    KMainKt$Test$2.class 910
    KMainKt$Test$3.class 1029
    KMainKt$Test$4.class 1450
    KMainKt$Test$5.class 1222
    KMainKt$Test$cb$1.class 1035

    Понятно откуда взялись первые два файла – это классы, которые описаны в нашей программе:


    • Файл «BB.class» — это объявленный в тексте класс.
    • Файл «KMainKt.class» — это основной класс программы Kotlin, в который включены все данные и методы, не включенные в классы. Имя этого файла состоит из имени файла исходной программы (в нашем случае это «kMain.kt») и суффикса «Kt». В нашем примере в этот класс помещены все используемые методы, такие как «Action», «cAction» и т.д.

    Откуда взялись остальные файлы?


    Эти файлы содержат в себе код классов, которые компилятор сгенерировал автоматически. В тексте программы приведенной выше, в комментариях, указаны имена сгенерированных классов, в соответствии с местом их объявления в программе.


    ВНИМАНИЕ: Компилятор создает каждый раз новый КЛАСС, а не новый экземпляр какого-то класса. Т.е. при каждом использовании кода в программе создается новый класс, который используется в единственном месте программы!


    Если посмотреть исходный текст, то можно обратить внимание на то, что текст, казалось-бы, не имеющий отношения к созданию временных объектов, тоже приводит к генерации классов.


      Action(::statMethod)             // jm/test/ktest/KMainKt$Test$4.INSTANCE
      cAction("text", String::length)  // jm/test/ktest/KMainKt$Test$5.INSTANCE

    Это объясняется тем, что для обеспечения используемого в тексте программы синтаксиса компилятор просто создает классы обертки, вызывающие указанные методы.


    В таблице выше видно, что генерируемые файлы классов довольно большие большие, насоразмерно коду, который выполняется в блоках кода для которых они были созданы. Несмотря на то, что в примере объем собственно полезного кода практически нулевой, объем созданных классов почти в 4 раза превышает объем основного файла программы "KMainKt.class". Причина этого в том, что в файлах классов сохраняется очень много дополнительной информации и, если объем кода класса маленький, количество полезной информации в файле класса будет составлять небольшую его часть.


    В случае, если код, который выполняют автоматические классы большой, то объем результирующих файлов можно игнорировать. Если же в большинстве случаев код для автоматического класса очень мал, то назвать создание файлов по 1-1.5Кб для каждого анонимного блока описанного в программе эффективным или даже приемлемым довольно сложно.


    Автоматически генерируемые классы напрямую влияют на размер программы в собранном виде. При активном использовании анонимного кода в программе ее размер может стремительно расти. Во многих случаях размер программы может играть существенную роль. Это может касаться как мобильных или WEB приложений, так и приложений, предназначенных для выполнения на персональных компьютерах.


    Дублирование. Оптимизация?


    Проведем еще один эксперимент: проверим способности Kotlin по оптимизации автоматических классов.


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


    Для проверки возможностей оптимизации просто размножим две строчки кода. Одну, использующую пользовательский код, и вторую, использующую полностью автоматический код.


    Action{}
    Action{}
    
    Action( ::statMethod )
    Action( ::statMethod )

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


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


    Для каждого места описания кода будет создан новый уникальный класс!


    К сожалению, в текущей версии Kotlin (используется версия «1.1-М04») никакой оптимизации, связанной с генерацией автоматических классов не существует и при любом использовании кода будет создан новый класс.



    Как бороться с "лишним" размером?


    В программах на Kotlin использование анонимного кода распространено очень широко и, без сомнения, легкость его описания и использования, является одним из принципиальных достоинств этого языка. В связи с обилием применений анонимного кода, проблема роста объема программы из-за генерации классов для каждого фрагмента кода может стать очень большой проблемой.


    Существует два способа борьбы с хранением избыточной информации в файле с собранным приложением.


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


    Одним из очень интересных инструментов этой группы является ProGuard. Это свободно распространяемый инструмент, который позволяет перепаковать собранный JAR архив с приложением и удалить из него всю служебную информацию. Помимо простого удаления служебной информации он производит очень большую работу по оптимизации кода классов. В качестве бонуса этот инструмент можно использовать как "Obfuscator", т.е. средство, которое затрудняет восстановление логики приложения по его коду.
    Это мощный инструмент, но его обзор выходит за пределы целей этой статьи.


    Второй способ (который можно сочетать с первым) заключается в том, чтобы использовать возможности компилятора Kotlin. Эта возможность рассмотрена в следующем разделе.


    Внутренние автоматические классы


    Проведем эксперимент с нашим примером, изменив описание двух методов.


    inline fun <T> cAction(v : T, cb : T.() -> Int) = println(v.cb())
    inline fun Action(cb : () -> Int) = println(cb())

    В этом случае мы добавили ключевое слово inline к писанию функций.


    После компиляции программы список файлов созданных компилятором для автоматических классов сократился до трех: «KMainKt$Test$1.class», «KMainKt$Test$2.class» и «KMainKt$Test$cb$1.class». Т.е. остались только те классы, которые не передаются в качестве параметров функциям «Action» и «cAction». При этом размер файла «KMainKt.class» увеличился примерно на 200 байт.


    Куда делись остальные классы?


    Код всех функций, объявленных с модификатором inline, был подставлен непосредственно в место вызова функций. Помимо этого компилятор создал и отдельные функции с таким же именем. Использовать объявленные функции синтаксис языка не позволит т.к. при попытке ее вызова ее код будет помещен в место использования, но ее статическая копия создается для обеспечения доступа к этой функции через reflection.


    Как это работает?


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


    При создании автоматических классов Kotlin может создать два типа объектов:


    1. Объект какого-то класса, наследника пользовательского класса.
    2. Объект, представляющий специальный класс для вызова блока кода.

    Объекты пользовательского типа


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


    Код
    fun add(i : ActionListener) {}
    
      add(object : ActionListener {
        override fun actionPerformed(e : ActionEvent?) {}
      })
    
      add(ActionListener { e -> })

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


    Реализация и способ вызова таких классов выглядит примерно так
    // реализация для синтаксиса: add(ActionListener { e -> })
    
    class AutoClassForCase1 : ActionListener {
        override fun actionPerformed(e : ActionEvent?) {
           // Сюда будет помещен анонимный код из блока лямбда-функции
        }
    
        companion object {
          @JvmField val INSTANCE = AutoClassForCase1()
        }
    }
       add( AutoClassForCase1.INSTANCE ) // Передача заранее созданного объекта

    Захват локальных переменных


    Kotlin, в отличии от Java, позволяет анонимному коду захватывать локальные переменные любого типа (в Java они должны быть "effectively final"), поэтому передача захваченных переменных автоматическим класса происходит через специальный класс "Ref<T>". В результате такого подхода, если локальная переменная была захвачена хотя бы одним блоком кода, то она автоматически оборачивается ссылкой в месте ее использования, и все обращения к ней идут с использованием этой ссылки. Такой способ вносит некоторые дополнительные накладные расходы в сравнении со способом, используемым в Java, но позволяет менять значения переменных.


    Код захвата локальной переменной

    При захвате блоком локальной переменной код:


      fun Test() {
        val local = 10
        Action{ local+1 }
        local + 2
      }

    превращается в такой:


      fun Test() {
        val local = Ref<Int>(10)   // Локальная переменная оборачивается ссылкой автоматически
        Action{ local.value + 1 }  // Обращение к захваченной переменной из блока кода
        local.value + 2            // Обращение к локальной переменной
        local.value = null         // Код генерируемый компилятором в конце функции 
      }

    При этом, нужно обратить внимание, на следующие моменты:


    1. Обращение к значению захваченной переменной из блока кода происходит через объект автоматического класса, который содержит тот же объект, что и ссылка в теле функции. Этот объект создается и передается в конструктор объекта автоматического класса в месте объявления блока кода.
    2. В случае, если код захватывает какие-либо переменные из текущего контекста, в качестве объекта автоматического класса используется не синглетон, а создается новый объект. Это происходит для того, чтобы была возможность вызвать конструктор и передать ссылки на захваченные переменные.
    3. В конце функции Kotlin автоматически генерирует код очистки объекта для всех захваченных переменных, таким образом, после завершения функции, число ссылок на локальные объекты всегда равно числу созданных автоматических объектов.

    Объекты для блока кода


    Описанный чуть выше пример для вызова анонимного блока кода, реализуется компилятором примерно следующим способом:


    Реализация анонимного блока кода
      // Автоматически создаваемый класс для вызова кода 
      class AutoClassForMethod : Function0<Int> {
    
         //Захваченные переменные располагаются в полях этого класса
         private val local : Ref<Int>
    
         //Ссылки на захваченные переменные передаются через конструктор
         constructor( l:Ref<Int> ) { local.value = l.value }
    
         //Метод вызываемый при вызове кода
         fun invoke() : Int {
            // Здесь будет размещен пользовательсий код
         }
      }
    
      // Создание нового автоматического объекта и передача ему захваченных переменных 
      Action( AutoClassForMethod(local) )

    В этом примере, для простоты, я опустил прослойку промежуточных преобразований параметров из Any (фактически Object т.к. эта часть кода Kotlin написана на Java) в конкретные типы т.к. это никак не влияет на понимание.


    Все блоки кода Kotlin всегда имеют тип FunctionX<Ret[,Param1[,...]]>, где FunctionX — это набор классов шаблонов, реализующих вызов метода с произвольным количеством параметров. Последний тип шаблона — это тип возвращаемого значения, первый — это тип первого параметра и т.д. Символ Х в имени класса я использовал для обозначения количества параметров метода. Т.е. метод, описываемый в Kotlin как ()->Unit будет иметь фактический тип Function0<Unit>, а метод описываемый как (Int,String)->Double тип Function2<Int,String,Double> и т.д.


    Шаблоны FunctionX описаны в библиотеке Kotlin "kotlin-runtime.jar" и их можно использовать в своей программе. Синтаксис описания блоков кода с помощью "стандартного" синтаксиса в виде ()->Unit и с помощью использования шаблона Function0<Unit> абсолютно эквивалентен. В этом можно убедиться, заменив в первоначальном примере объявление функций.


    fun <T> cAction(v : T, cb : Function1<T,Int>)  = println(cb.invoke(v))
    fun Action(cb : Function0<Int>) = println(cb.invoke())

    Программа будет собираться и выполняться по прежнему.
    Обратите внимание на то, что синтаксис "T.() -> Int" использованный ранее, абсолютно прозрачно был заменен на использование шаблона с передачей в него лишнего типа, соответствующего типу объекта для которого будет вызван метод. Т.е. разница между вызовом статической функции и функции объекта, только в лишнем параметре "this".


    Что в этом полезного?


    С точки зрения простого пользователя доступного синтаксиса языка — полезного здесь немного.


    Но, с точки зрения разработчика какой-либо библиотеки, или человека, реализующего какой-то сложный алгоритм управления с использованием динамических списков методов, знание того как устроены автоматические объекты и как обращаться с блоками кода может оказаться крайне полезным.


    Пригодится это значение или нет — может решить только каждый самостоятельно.



    Особенности использования inline-функций


    Официальное описание применения модификатора inline находится тут. Ниже я постараюсь кратко описать результат применения этого модификатора и различия кода с его использованием и без.


    Применения модификатора inline к функции говорит компилятору о том, что ее код нужно вставлять в место ее использования. Т.е. этот модификатор работает аналогично одноименному модификатору языка "С++", но прямая аналогия с ним будет неверной. Реализация inline-функций в Kotlin является чем-то средним между inline-функциями и макросами языка "С++". Они обеспечивают полную синтаксическую проверку типов и использования как функции, но при этом ведут себя и используются как макросы.


    Важной особенностью использования модификатора inline является то, что он действует не только на функцию, но и на ее параметры. Если параметром функции является блок кода, то этот модификатор будет применен и к блоку тоже. Т.е. код этого блока будет считаться "inline" со всеми его особенностями и тоже будет подставлен в место использования.


    Изменение inline-типа параметров у inline-функций


    Блок кода, к которому применен модификатор "inline" не существует в виде объекта, поэтому такой блок не может быть использован в качестве данных. Его нельзя сохранить в переменной, передать в качестве параметра функции объявленной без модификатора "inline", получить его тип и т.д.


    Ошибка: невозможно сохранить объект inline-функции
    class Holder {
      var cb : (()->Unit)? = null
    
      inline fun setup( cb:()->Unit ) {
        this.cb = cb // Ошибка: невозможно сохранить объект inline-функции
      }
    }
    fun Test() {
      Holder().setup{ }
    }  

    Иногда возникает необходимость иметь одновременно и inline и не-inline блок в качестве параметра одной и той же функции. К примеру, когда нужно выполнить часть кода в момент вызова, а вторую часть после каких-то сторонних действий. Если известно, что блок кода действий невелик, то имеет смысл использовать inline-функцию, а для того, чтобы вторая часть была сгенерирована в виде объекта нужно модификатор "noinline".


    Пример использования noinline
    class Holder {
      var cb : (()->Unit)? = null
    
      inline fun setup( cb:()->Unit, noinline delayed:(()->Unit)?=null ) {
        this.cb = delayed
        cb()
        CallCodeWithDelay()
      }
    
      fun DelayPassed() = cb?.invoke()
    }

    Безусловная подстановка


    В С++ модификатор inline является пожеланием и компилятор волен реализовывать подстановку кода или нет. В Kotlin этот оператор является безусловным. В документации об этом явно не указано, а самостоятельно я не смог найти ситуаций, в которых бы функции с использованием такого модификатора не были бы подставлены в место использования. Судя по различному поведению кода, самоволия по реализации таких функций компилятор проявлять не может. Функции с модификатором "inline" всегда будут подставлены в место их использования.


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


    @Suppress("NOTHING_TO_INLINE")
    inline fun SetupControl( ctl: Label ) {
      ctl.text = "Label"
      ctl.setSize(  100,100 )
    }
    
    fun Test() {
      SetupControl( Label() )
    }

    В этом примере функция SetupControl используется в качестве "умного" макроса, который будет расширен в месте его использования.


    Изменение области видимости кода


    Второе отличие как от "С++", так и от любой функции вообще, заключается в изменении области видимости для кода, который передается функции с объявлением inline и без него.


    Как известно, из кода анонимной функции в Kotlin нельзя выйти с помощью return. В случае, если функция в нашем примере объявлена не inline, то код ниже не скомпилируется.


    fun Test1() : Int {
      Action { return 3 }
    }  

    Если же добавить к описанию функции inline, то этот код скомпилируется успешно, но оператор return будет возвращаться не из анонимного кода, а из той функции, где этот код размещен. Т.е. возврат будет осуществлен не из анонимного блока кода, а полностью из функции "Test". Вернуть значение из блока с помощью оператора return по прежнему невозможно и нужно использовать стандартный способ — результат последнего выражения.


    Точная информация о типах параметров шаблона


    Следствием того, что все действия функции объявленной inline подставляются непосредственно в место ее использования, является то, что в тексте ее кода становится доступна точная информация о типах переданных ей параметров.


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


    Ошибка: фактический тип неизвестен
    fun <T,P> templateTest(value : T) : P {
      if ( value is P )  // Ошибка: фактический тип неизвестен
        return value
    }
    
    templateTest<String,String>("text")

    Для обеспечения доступа к фактическим типам шаблонных праметров inline-функции нужно указать специальный модификатор "reified".


    inline fun <T,reified P> templateTest(value : T) : P {
      if ( value is P )  // В этом случае тип известен и проверка возможна
        return value
    }

    Правда такая особенность может быть использована только в момент компиляции. Несмотря на то, что функция с именем templateTest будет сгенерирована, попытка ее выполнить приведет к ошибке. В настоящее время, в Kotlin реализация функций с reified, создана с прицелом на использование глобального
    хранилища для всех таких типов, но функции преобразования закрыты заглушками, поэтому, если получить доступ к такой функции через refrection, то при выполнении в ней кода, основанного на использовании reified, будет выброшено исключение.


    This function has a reified type parameter and thus can only be inlined at compilation time, not called directly

    Виртуальность inline-функций


    Функции, объявленные с модификатором inline можно считать неким макросом, не существующим в виде отдельного объекта.
    Получить доступ к объекту такой функции в синтаксисе Kotlin невозможно. Нельзя вызвать такую функцию и передать ей параметры. Нельзя организовать рекурсию вызова этой функции.



    Итого: плюсы и минусы, выводы


    Плюсы inline-функций
    • Код таких функций будет выполняться быстрее т.к. уменьшено число накладных расходов, связанных с вызовом методов и передачей им параметров.


    • Функции выполняются в контексте того кода, в котором их вызвали, поэтому они не захватывают переменные контекста. Захватываемые анонимным кодом, выделяемым в отдельный класс, переменные, в «Kotlin», оборачиваются в специальный объект «Ref». Этот объект хранит локальную переменную, поэтому при любом обращении к ней фактически происходит обращение в полю класса. Такой способ приводит к дополнительным накладным расходам при любом ее использовании, даже за пределами анонимного кода. inline-функции лишены такого недостатка, они имеют прямой доступ ко всем локальным переменным и использование таких функций не приводит к усложнению доступа для остального кода функции к захваченным переменным..


    • Для обеспечения выполнения таких функций не генерируются автоматические классы, поэтому итоговый размер программы зависит только от частоты их использования, а не от размеров файлов.


    • При использовании inline-функций можно получить полный доступ к типу ее шаблонных праметров.

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


    • Такие функции нельзя использовать как объект: нельзя передать их в качестве параметра, сохранить в переменой, получить их тип и т.д.


    • Существуют ограничения на код, который может быть выполнен в таких функциях. Любые действия, которые требуют вызова функции будут невозможны.
      Например, нельзя организовать рекурсивный вызов такой функции.


    • Как следует из спецификации JVM для Java 7 и аналогичного документа о Java 8 размер кода одного метода не может превышать величину в 65534 байт (т.е. 65536 байт допустимых стандартом, минус два байта на решения исторической проблемы с ошибкой в виртуальной машине). Код inline-функций размещается непосредственно в месте их использования и возможна ситуация, когда будет достигнут лимит на размер метода. Ситуация маловероятная, но она может возникнуть.

    На мой взгляд, применимость inline-функций крайне широка. Достаточно взглянуть на реализацию стандартной библиотеки Kotlin, чтобы увидеть что его создатели со мной полностью согласны — в ней довольно сложно найти функции без этого модификатора.


    При выборе как именно описать ту или иную функцию, можно использовать довольно простые правила.
    Удобнее всего будет реализовать ее в виде inline-функции, если:


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


    • Она принимает в качестве параметра один или более блоков анонимного кода, над которыми не нужно проводить операции с объектами (т.е. их не нужно передавать в не-inline-функции или сохранять в переменные). Использование inline-функции в таком случае оправдано до тех пор, пока объем ее кода не превышает объем, который будет сгенерирован при передаче ей анонимных блоков в виде автоматических классов, что, как видно из таблицы в начале статьи, составляет около полутора килобайт на каждый блок кода. Т.е. если передается один анонимный блок, то функцию выгоднее сделать inline если размер ее кода менее полутора килобайт, если два блока — менее трех килобайт и т.д.


    • Для обеспечения работоспособности алгоритма необходимо иметь информацию о типах параметров шаблона. В этом случае, просто нет другого способа реализовать такой алгоритм.


    • Код, реализуемый такой функцией уникален и вероятность его повторного использования крайне мала. В этом случае не играет абсолютно никакой роли — будет ли ее код вставлен в месте использования сразу или выделен в отдельную функцию. Повторное использование такого кода маловероятно или лишено смысла.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 13

      0
      но пока везде где это возможно нужно использовать этот модификатор (inline)

      Вредный совет.
      Inline стоит использовать так где он действительно нужен.

        0

        Обоснование моего вывода описано выше.
        Обоснование того что это вредно привести можно?


        Так же было бы очень интересно узнать о том, где inline нельзя использовать или хотя бы те, где его наличие и отсутствие хоть на что-то влияло бы.

          0
          Обоснование моего вывода описано выше.

          Я так и не понял практического смысла. Да у вас jar будет на пару кб больше. И?


          Обоснование того что это вредно привести можно?

          Очень просто, если у вас размер метода такими инлайнами вырастет за определенный размер(емнип около 35 инструкций), к нему не будут применяться многие оптимизации JIT.


          Но меня это мало интересует, меня больше интересует чистота кода, и inline который используется от балды есть плохо пахнущий код, имо.

        0
        Я так и не понял практического смысла. Да у вас jar будет на пару кб больше. И?

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


        fun Action( cb:()->Unit ) {}
        
        fun statMethod() {}
        
        fun Test() {
          Action( ::statMethod )  // эта конструкция
        }

        Будет сгенерирован очередной уникальный класс, который от прошлого использования такой конструкции будет иметь всего одно отличие: номер в имени.
        Если Вас абсолютно не волнует какой код будет сгенерирован компилятором и как он будет работать, лишь бы программа выполнялась, то эту статью можно с чистой совестью забыть.
        Меня лично такие вещи интересуют и "плохо пахнущим" кодом я как раз считаю такой, где разработчику абсолютно дофонаря какую именно конструкцию использовать.
        Но это мои тараканы, я не навязываю :)


        Да у вас jar будет на пару кб больше. И?

        Насчет пары кб — это откуда взялось?
        В тексте выше же есть цифры.
        КАЖДОЕ использование такой конструкции добавит около полутора Кб.
        Если их в коде используется сто, то будет + 1.5Мб...


        Вообще, эта статья родилась как результат исследования, которым я занялся, когда с удивлением обнаружил что тест для моего DSL, не имея практически никакого кода с текстом на пру экранов, занимает в 4 раза больше, чем лежащая рядом софтина в размером исходников в пару сотен Кб.
        Заинтересовался, выяснил отчего так происходит.
        Решил поделиться.


        Очень просто, если у вас размер метода такими инлайнами вырастет

        Либо я чего-то в принципе не понимаю, либо Вы.
        О размере какого метода идет речь?
        Код, который генерируется в месте объявления автоматического класса, абсолютно никак не зависит от того в каком именно файле этот класс физически расположен.
        Наличие inline влияет только на место расположения сгенерированного класса и не более того.

          0

          По первой части: если это действительно проблема, то нужно написать разработчикам в слаке, или еще лучше завести issue youtrack:
          https://youtrack.jetbrains.com/issues/KT
          http://kotlinslackin.herokuapp.com/


          В противном случае вы только учитите плохому.


          По второй части — 3 часа ночи, завтра на свежую голову перечитаю.

          0
          если это действительно проблема

          С точки зрения меня (если поставить себя на место разработчиков) довольно сложно называть это проблемой, и тем более проблемой именно Kotlin.


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


          На мой взгляд описанное в статье — это скорее "фича", которую имеет смысл знать и использовать, а не какая-то проблема.


          Проблемой можно назвать отсутствие какой-нибудь оптимизации, но, я уверен, это известно и без меня и, если язык будет развиваться, она появится.


          ПС: Повторно прошу обосновать утверждение о вреде, которому я "учу".

            0
            Разработчики сохраняют классы в отдельные файлы а, при использовании inline, пользуются возможностью появившейся в Java, которая позволяет разместить в одном файле несколько классов.


            А это что за возможность такая?
              0

              Я имел в виду то, что в Java с какой-то версии (1.2 или 1.4 — не разбирался точно) появилась возможность в одном файле класса описывать сколько угодно классов верхнего уровня.
              Но в первоначальной статье у меня была ошибка, поэтому в текущем варианте это уже не важно.

              0
              Я с деталями реализации Kotlin не знаком, а почему они не пошли по пути лямбд Java, где класс-реализация генерируется автоматически в райнтайме через LambdaMetafactory#metafactory?
                0

                Я, в свою очередь, не знаком с деталями Java, но то что я вижу сейчас в сгенерированном из Java коде производит абсолютно такие же действия, как и компилятор Kоtlin: создается новый класс для каждого места использования лямбды и ему в конструкторе передаются захваченные переменные.
                Насколько я понял "LambdaMetafactory" — это возможность для пользователя. Компилятор Java ее не использует.


                Сейчас реализация Kotlin описана более подробно в текущем варианте статьи выше.

                  0
                  Нет, Java (компилятор) не создает новый класс для каждого места использования лямбды (если речь о 1.8 -> 1.8), а использует LambdaMetafactory/invokedynamic. Это легко проверяется компиляцией вот такого кода:
                  import java.util.function.Supplier;
                  
                  public class Test {
                  
                      public static void main(String... args) {
                          String hello = "hello";
                          print(() -> hello + ", world!");
                      }
                  
                      public static <T> void print(Supplier<T> s) {
                          System.out.println(s.get());
                      }
                  }
                  


                  И последующей декомпиляцией через «javap -verbose -p Test»

                  P.S. В рантайме да, создается класс, насколько я помню.
                    0

                    В любом случае создается класс.
                    Какая разница как это делается, компилятором при сборке программы или генерацией его кода на лету через миллион библиотечных прослоек?
                    Лично мне, после беглого разглядывания java\lang\invoke*.java, гораздо более симпатичен подход, который используется именно в Kotlin.


                    ПС: Я не думаю что в Kotlin изменят способ обработки кода т.к. одной из задач этого компилятора собираться под мобильные платформы а, насколько я знаю, даже на Android, существенные проблемы с генерацией кода на лету.
                    Но я не специалист, поэтому в обсуждение ввязываться не буду :)

                  0

                  Потому что до версии 1.1 Котлин таргетится на байткод 1.6. После релиза 1.1 (который должен быть довольно скоро) будет возможность указывать таргет 1.8, там будет именно так.

                Only users with full accounts can post comments. Log in, please.