Kotlin. Встроенные (inline) функции
Модификатор inline влияет и на функцию, и на лямбду, переданную ей: они обе будут встроены в место вызова.
Разница между ними в том, что встраиваемая лямбда может быть вызвана только внутри inline-функции, либо может быть передана в качестве встраиваемого аргумента. В то время как noinline-лямбды можно хранить внутри полей, передавать куда-либо итд.
Return
Но если лямбда-выражение передаётся невстраиваемой функции, то использование оператора return недопустимо. Если же лямбда-выражение передаётся в inline-функцию, то оператор return разрешён и он завершает работу этой функции.
Reified
Параметры, отмеченные этим ключевым словом, ещё называют овеществляемыми.
Допустим у нас есть такая функция:
За кулисами компилятор заменит тип T на фактический, поэтому мы сможем получить о нём информацию и при этом нет необходимости этот тип явно передавать функции.
Также reified может быть использован в другом сценарии: для возврата из функции разных типов данных.
Или для замены ссылок на классы более лаконичным кодом.
Встроенная функция с reified не может быть вызвана из кода Java. Такие функции требуют дополнительной обработки для подстановки значения типовых аргументов в байт-код, и поэтому всегда должны быть встраиваемыми. А inline-функции можно вызвать из Java только как функции без встраивания.
Почему reified возможно использовать только с встроенными функциями?
Полезные ссылки
Встроенные (inline) функции
Использование функций высшего порядка влечёт за собой снижение производительности: во-первых, функция является объектом, а во-вторых, происходит захват контекста замыканием, то есть функции становятся доступны переменные, объявленные вне её тела. А выделения памяти (как для объекта функции, так и для её класса) и виртуальные вызовы занимают системные ресурсы.
Но во многих случаях эти «накладные расходы» можно устранить с помощью инлайнинга (встраивания) лямбда-выражений. Например, функция lock() может быть легко встроена в то место, из которого она вызывается:
Вместо создания объекта функции для параметра и генерации вызова, компилятор мог бы выполнить что-то подобное этому коду:
Разве это не то, чего мы хотели изначально?
Чтобы заставить компилятор поступить именно так, нам необходимо отметить функцию lock модификатором inline :
Модификатор inline влияет и на функцию, и на лямбду, переданную ей: они обе будут встроены в место вызова.
Встраивание функций может увеличить количество сгенерированного кода, но если вы будете делать это в разумных пределах (не инлайнить большие функции), то получите прирост производительности, особенно при вызове функций с параметрами разного типа внутри циклов.
noinline
В случае, если вы хотите, чтобы только некоторые лямбды, переданные inline-функции, были встроены, вам необходимо отметить модификатором noinline те функции-параметры, которые встроены не будут:
Заметьте, что если inline-функция не имеет ни inline параметров, ни параметров вещественного типа, компилятор выдаст предупреждение, так как встраивание такой функции вряд ли принесёт пользу (вы можете скрыть предупреждение, если уверены, что встраивание необходимо).
Нелокальные return
В Kotlin мы можем использовать обыкновенный, безусловный return только для выхода из именованной функции или анонимной функции. Это значит, что для выхода из лямбды нам нужно использовать label. Обычный return запрещён внутри лямбды, потому что она не может заставить внешнюю функцию завершиться.
Но если функция, в которую передана лямбда, встроена, то return также будет встроен, поэтому так делать можно:
Такие return (находящиеся внутри лямбд, но завершающие внешнюю функцию) называются нелокальными ( non-local ). Мы используем такие конструкции в циклах, которые являются inline-функциями:
Заметьте, что некоторые inline-функции могут вызывать переданные им лямбды не напрямую в теле функции, а из иного контекста, такого как локальный объект или вложенная функция. В таких случаях, нелокальное управление потоком выполнения также запрещено в лямбдах. Чтобы указать это, параметр лямбды необходимо отметить модификатором crossinline :
break и continue пока что недоступны во встроенных лямбдах, но мы планируем добавить их поддержку
Параметры вещественного типа
Иногда нам необходимо получить доступ к типу, переданному в качестве параметра:
В этом примере мы осуществляем проход по дереву и используем рефлексию, чтобы проверить узел на принадлежность к определённому типу. Это прекрасно работает, но вызов выглядит не очень симпатично:
Что мы на самом деле хотим, так это передать этой функции тип, то есть вызвать её вот так:
В таких случаях inline-функции могут принимать параметры вещественного типа (reified type parameters). Чтобы включить эту возможность, мы можем написать что-то вроде этого:
Хотя рефлексия может быть не нужна во многих случаях, мы всё ещё можем использовать её с параметром вещественного типа:
Обычная функция (не отмеченная как встроенная) не может иметь параметры вещественного типа. Тип, который не имеет представление во времени исполнения (например, параметр невещественного или фиктивного типа вроде Nothing ), не может использоваться в качестве аргумента для параметра вещественного типа.
Для низкоуровневого описания см. спецификацию.
Встроенные (inline) свойства (с версии 1.1)
Модификатор inline можно применять к методам доступа свойств, у которых нет теневых полей (backing field). Вы можете аннотировать отдельные методы доступа:
Также можно аннотировать свойство. В этом случае оба его метода доступа будут отмечены как встроенные:
В месте вызова встроенные методы доступа встраиваются как обычные inline-функции.
Ограничения для встроенных функций в public API
Это может привести к появлению двоичной несовместимости, если модуль, который объявляет встроенную функцию, был изменён, но не перекомпилировался после внесения этих изменений.
Встроенные (inline) функции
Использование функций высшего порядка влечёт за собой снижение производительности: во-первых, функция является объектом, а во-вторых, происходит захват контекста замыканием, то есть функции становятся доступны переменные, объявленные вне её тела. А выделения памяти (как для объекта функции, так и для её класса) и виртуальные вызовы занимают системные ресурсы.
Но во многих случаях эти «накладные расходы» можно устранить с помощью инлайнинга (встраивания) лямбда-выражений. Например, функция lock() может быть легко встроена в то место, из которого она вызывается:
Вместо создания объекта функции для параметра и генерации вызова, компилятор мог бы выполнить что-то подобное этому коду:
Разве это не то, чего мы хотели изначально?
Чтобы заставить компилятор поступить именно так, нам необходимо отметить функцию lock модификатором inline :
Модификатор inline влияет и на функцию, и на лямбду, переданную ей: они обе будут встроены в место вызова.
Встраивание функций может увеличить количество сгенерированного кода, но если вы будете делать это в разумных пределах (не инлайнить большие функции), то получите прирост производительности, особенно при вызове функций с параметрами разного типа внутри циклов.
noinline
В случае, если вы хотите, чтобы только некоторые лямбды, переданные inline-функции, были встроены, вам необходимо отметить модификатором noinline те функции-параметры, которые встроены не будут:
Заметьте, что если inline-функция не имеет ни inline параметров, ни параметров вещественного типа, компилятор выдаст предупреждение, так как встраивание такой функции вряд ли принесёт пользу (вы можете скрыть предупреждение, если уверены, что встраивание необходимо).
Нелокальные return
В Kotlin мы можем использовать обыкновенный, безусловный return только для выхода из именованной функции или анонимной функции. Это значит, что для выхода из лямбды нам нужно использовать label. Обычный return запрещён внутри лямбды, потому что она не может заставить внешнюю функцию завершиться.
Но если функция, в которую передана лямбда, встроена, то return также будет встроен, поэтому так делать можно:
Такие return (находящиеся внутри лямбд, но завершающие внешнюю функцию) называются нелокальными ( non-local ). Мы используем такие конструкции в циклах, которые являются inline-функциями:
Заметьте, что некоторые inline-функции могут вызывать переданные им лямбды не напрямую в теле функции, а из иного контекста, такого как локальный объект или вложенная функция. В таких случаях, нелокальное управление потоком выполнения также запрещено в лямбдах. Чтобы указать это, параметр лямбды необходимо отметить модификатором crossinline :
break и continue пока что недоступны во встроенных лямбдах, но мы планируем добавить их поддержку
Параметры вещественного типа
Иногда нам необходимо получить доступ к типу, переданному в качестве параметра:
В этом примере мы осуществляем проход по дереву и используем рефлексию, чтобы проверить узел на принадлежность к определённому типу. Это прекрасно работает, но вызов выглядит не очень симпатично:
Что мы на самом деле хотим, так это передать этой функции тип, то есть вызвать её вот так:
В таких случаях inline-функции могут принимать параметры вещественного типа (reified type parameters). Чтобы включить эту возможность, мы можем написать что-то вроде этого:
Хотя рефлексия может быть не нужна во многих случаях, мы всё ещё можем использовать её с параметром вещественного типа:
Обычная функция (не отмеченная как встроенная) не может иметь параметры вещественного типа. Тип, который не имеет представление во времени исполнения (например, параметр невещественного или фиктивного типа вроде Nothing ), не может использоваться в качестве аргумента для параметра вещественного типа.
Для низкоуровневого описания см. спецификацию.
Встроенные (inline) свойства (с версии 1.1)
Модификатор inline можно применять к методам доступа свойств, у которых нет теневых полей (backing field). Вы можете аннотировать отдельные методы доступа:
Также можно аннотировать свойство. В этом случае оба его метода доступа будут отмечены как встроенные:
В месте вызова встроенные методы доступа встраиваются как обычные inline-функции.
Ограничения для встроенных функций в public API
Это может привести к появлению двоичной несовместимости, если модуль, который объявляет встроенную функцию, был изменён, но не перекомпилировался после внесения этих изменений.
Inline functions
Using higher-order functions imposes certain runtime penalties: each function is an object, and it captures a closure. A closure is a scope of variables that can be accessed in the body of the function. Memory allocations (both for function objects and classes) and virtual calls introduce runtime overhead.
But it appears that in many cases this kind of overhead can be eliminated by inlining the lambda expressions. The functions shown below are good examples of this situation. The lock() function could be easily inlined at call-sites. Consider the following case:
Instead of creating a function object for the parameter and generating a call, the compiler could emit the following code:
To make the compiler do this, mark the lock() function with the inline modifier:
The inline modifier affects both the function itself and the lambdas passed to it: all of those will be inlined into the call site.
Inlining may cause the generated code to grow. However, if you do it in a reasonable way (avoiding inlining large functions), it will pay off in performance, especially at «megamorphic» call-sites inside loops.
noinline
If you don’t want all of the lambdas passed to an inline function to be inlined, mark some of your function parameters with the noinline modifier:
Inlinable lambdas can only be called inside inline functions or passed as inlinable arguments. noinline lambdas, however, can be manipulated in any way you like, including being stored in fields or passed around.
If an inline function has no inlinable function parameters and no reified type parameters, the compiler will issue a warning, since inlining such functions is very unlikely to be beneficial (you can use the @Suppress(«NOTHING_TO_INLINE») annotation to suppress the warning if you are sure the inlining is needed).
Non-local returns
In Kotlin, you can only use a normal, unqualified return to exit a named function or an anonymous function. To exit a lambda, use a label. A bare return is forbidden inside a lambda because a lambda cannot make the enclosing function return :
But if the function the lambda is passed to is inlined, the return can be inlined, as well. So it is allowed:
Such returns (located in a lambda, but exiting the enclosing function) are called non-local returns. This sort of construct usually occurs in loops, which inline functions often enclose:
Note that some inline functions may call the lambdas passed to them as parameters not directly from the function body, but from another execution context, such as a local object or a nested function. In such cases, non-local control flow is also not allowed in the lambdas. To indicate that the lambda parameter of the inline function cannot use non-local returns, mark the lambda parameter with the crossinline modifier:
break and continue are not yet available in inlined lambdas, but we are planning to support them, too.
Reified type parameters
Sometimes you need to access a type passed as a parameter:
Here, you walk up a tree and use reflection to check whether a node has a certain type. It’s all fine, but the call site is not very pretty:
A better solution would be to simply pass a type to this function. You can call it as follows:
To enable this, inline functions support reified type parameters, so you can write something like this:
Though reflection may not be needed in many cases, you can still use it with a reified type parameter:
Normal functions (not marked as inline) cannot have reified parameters. A type that does not have a run-time representation (for example, a non-reified type parameter or a fictitious type like Nothing ) cannot be used as an argument for a reified type parameter.
Inline properties
The inline modifier can be used on accessors of properties that don’t have backing fields. You can annotate individual property accessors:
You can also annotate an entire property, which marks both of its accessors as inline :
At the call site, inline accessors are inlined as regular inline functions.
Restrictions for public API inline functions
When an inline function is public or protected but is not a part of a private or internal declaration, it is considered a module’s public API. It can be called in other modules and is inlined at such call sites as well.
This imposes certain risks of binary incompatibility caused by changes in the module that declares an inline function in case the calling module is not re-compiled after the change.
To eliminate the risk of such incompatibility being introduced by a change in a non-public API of a module, public API inline functions are not allowed to use non-public-API declarations, i.e. private and internal declarations and their parts, in their bodies.
Kotlin и свои почти языковые конструкции
Скорее всего, из разработчиков, пользующихся Java, и в особенности Android-разработчиков многие уже знают про Kotlin. Если нет, то никогда не поздно узнать. Особенно если Java не устраивает вас чем-то как язык — что наверняка так — или если вы владеете Scala, но и этот язык вам не подходит, что тоже не исключено.
Если кратко, то Kotlin — это статически типизированный язык, ориентирующийся на JVM, Android (компилируется в байт-код Java) и веб (компилируется в JavaScript). JetBrains, разработчик языка, ставили своей целью лаконичный и понятный синтаксис, быструю компиляцию кода и типобезопасность. Язык пока находится в предрелизном состоянии, но всё стремительно движется к релизу.
К слову, после Java «переучиться» на Kotlin не составит никакого труда, в этом поможет и понятный (субъективно) синтаксис, и полная совместимость с кодом на Java в обе стороны, что позволяет Java-программисту использовать весь привычный набор библиотек.
Ещё одной целью разработчиков языка была возможность его гибкого использования, в том числе для создания библиотек, внешне похожих на DSL, и собственных конструкций (хороший пример типобезопасного builder’а для HTML; статья про реализацию yield). У Kotlin есть несколько фич, которые позволят решать эти задачи эффективно и красиво. Давайте с ними познакомимся.
Расширения (Extensions)
В Kotlin есть возможность дополнять функционал произвольного класса, не наследуясь от него, функциями (и свойствами) расширения. Такая же возможность есть, например, в C#. Стоит отметить, что поведение функций расширения отличается от member functions: вызовы функций расширения разрешаются статически, по объявленному типу, а не виртуально.
toRegex(), drop(n), take(n) и joinToString(» «) в примере — это тоже функции расширения.
Альтернативный синтаксис для вызова функций
1. Функцию экземпляра или функцию расширения, имеющую только один аргумент, можно вызывать в инфиксной форме:
2. Если последний аргумент функции имеет функциональный тип, то при вызове функции можно соответствующее лямбда-выражение писать за круглыми скобками, просто в фигурных:
Встраиваемые (inline) функции
Несмотря на то, что этот код пестрит лямбда-выражениями, ни одного анонимного класса для них создано не будет, и i даже не попадёт в closure. Просто праздник!
Попробуем?
Посмотрим, что мы можем сделать со всем этим арсеналом — предположим, что мы хотим сделать такую довольно бесполезную конструкцию:
Прямо так, к сожалению, не получится, но постараемся сделать что-то похожее.
Первая попытка: функция с двумя функциональными аргументами
Вот так её можно использовать:
Если сюда добавить inline, должно получиться достаточно эффективно. Позже посмотрим, насколько, а пока попробуем добиться более красивого синтаксиса для этой конструкции.
Вторая попытка: красивый синтаксис
Этот вариант не рассчитан на многопоточность! Для использования его в нескольких потоках нужно будет завернуть экземпляры в ThreadLocal, что ещё немного ухудшит производительность.
Третья попытка: каррирование
По сравнению с первой попыткой изменилось расположение скобок в вызове. 🙂
Учитывая inline, мы можем ожидать, что работать этот вариант будет так же, как первый.
Это можно проверить, взглянув на байт-код: у IntelliJ IDEA есть утилита, показывающая байт-код, в который скомпилируется код на Kotlin, на лету, и даже можно посмотреть, как будет отличаться байт-код с @inline и без.
Производительность
Давайте теперь посмотрим, что будет с производительностью нашей конструкции в разных вариантах.
Тестировать будем на таком примере:
Задно добавим к сравнению такой код, который будет эталоном производительности:
По итогам пятидесятикратного запуска с последующим усреднением времени получилась следующая таблица:
| Реализация | Без inline | C inline |
|---|---|---|
| Эталон | 319 ms | |
| I попытка | 406 ms | 348 ms |
| II попытка | 610 ms | 520 ms |
| II попытка с ThreadLocal | 920 ms | 876 ms |
| III попытка | 413 ms | 399 ms |
Как видно, производительность более простых первого и третьего вариантов достаточно близка к эталону, в некоторых случаях читаемость кода можно «купить» за такое увеличение времени работы. Вариант с более красивым синтаксисом устроен сложнее и работает, соответственно, дольше, но если хочется конструкций, совсем похожих на DSL, то и он вполне применим.
Итого
Kotlin предоставляет действительно гибкие возможности для «кастомизации» языка, но за них иногда будет нужно платить производительностью. Аннотация @inline может помочь улучшить ситуацию, если в вашем коде есть функции первого порядка. В любом случае, думаю, у вас найдутся хорошие сценарии применения для всего этого.



