когда использовать встроенную функцию в Котлине?

108

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

lock(l) { foo() }

Вместо создания объекта функции для параметра и генерации вызова компилятор может выдать следующий код. ( Источник )

l.lock()
try {
  foo()
}
finally {
  l.unlock()
}

но я обнаружил, что не существует функционального объекта, созданного kotlin для не встроенной функции. Зачем?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}
Холи-Ява
источник
7
Для этого есть два основных варианта использования: один - с определенными типами функций высшего порядка, а другой - с параметрами овеществленного типа. Документация по встроенным функциям охватывает это: kotlinlang.org/docs/reference/inline-functions.html
zsmb13
2
@ zsmb13, спасибо, сэр. но я не понимаю этого: «Вместо создания объекта функции для параметра и генерации вызова компилятор может выдать следующий код»
holi-java
2
Я тоже не понимаю этого примера.
filthy_wizard 07
Что заставляет вас говорить, что inlineфункция может улучшить производительность и что это будет заметно для пользователя?
Игорь Ганапольский

Ответы:

285

Допустим, вы создали функцию более высокого порядка, которая принимает лямбду типа () -> Unit(без параметров, без возвращаемого значения) и выполняет ее следующим образом:

fun nonInlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

На языке Java это будет выглядеть примерно так (упрощенно!):

public void nonInlined(Function block) {
    System.out.println("before");
    block.invoke();
    System.out.println("after");
}

А когда звонишь из Котлина ...

nonInlined {
    println("do something here")
}

Под капотом здесь Functionбудет создан экземпляр , который обертывает код внутри лямбда (опять же, это упрощено):

nonInlined(new Function() {
    @Override
    public void invoke() {
        System.out.println("do something here");
    }
});

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


С другой стороны, если вы используете inlineключевое слово:

inline fun inlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

Когда вы называете это так:

inlined {
    println("do something here")
}

Ни один Functionэкземпляр не будет создан, а код вокруг вызова blockвнутри встраиваемой функции будет скопирован на место вызова, так что вы получите что - то подобное в байткоде:

System.out.println("before");
System.out.println("do something here");
System.out.println("after");

В этом случае новые экземпляры не создаются.

zsmb13
источник
21
В чем преимущество обертки объекта Function? т.е. почему не все встроено?
Arturs Vancans
14
Таким образом, вы также можете произвольно передавать функции в качестве параметров, сохранять их в переменных и т. Д.
zsmb13
2
Вы можете, и если вы сложные вещи с ними, вы будете в конечном итоге хотите знать о noinlineи crossinlineключевых словах - смотрите документы .
zsmb13
2
В документации указывается причина, по
CorayThan
2
Но вы не ответили на вопрос «Когда использовать». Вы только что объяснили, как это используется. Ответ на этот вопрос находится в документации и указывает на то, что если у вас есть функция с небольшим объемом кода, и функция часто используется, то использование встроенной функции может повысить производительность.
AndroidDev
45

Позвольте мне добавить: Когда не использоватьinline :

  1. Если у вас есть простая функция, которая не принимает другие функции в качестве аргумента, не имеет смысла встраивать их. IntelliJ предупредит вас:

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

  2. Даже если у вас есть функция «с параметрами функциональных типов», вы можете столкнуться с тем, что компилятор сообщит вам, что встраивание не работает. Рассмотрим этот пример:

     inline fun calculateNoInline(param: Int, operation: IntMapper): Int {
         val o = operation //compiler does not like this
         return o(param)
     }

    Этот код не компилируется, что приводит к ошибке:

    Незаконное использование строчного параметра "операция" в "...". Добавьте в объявление параметра модификатор noinline.

    Причина в том, что компилятор не может встроить этот код, особенно operationпараметр. Если operationон не заключен в объект (что было бы результатом применения inline), как он вообще может быть назначен переменной? В этом случае компилятор предлагает аргументировать noinline. Не имеет смысла иметь inlineфункцию с одной noinlineфункцией, не делайте этого. Однако, если существует несколько параметров функциональных типов, при необходимости рассмотрите возможность встраивания некоторых из них.

Итак, вот несколько предлагаемых правил:

  • Вы можете встроить, когда все параметры функционального типа вызываются напрямую или передаются другой встроенной функции
  • Вы должны встраивать, когда имеет место ^.
  • Вы не можете встроить, когда параметр функции назначается переменной внутри функции
  • Вам следует рассмотреть возможность встраивания, если хотя бы один из параметров вашего функционального типа может быть встроен, используйтеnoinline для остальных.
  • Вы не должны встраивать огромные функции, думайте о сгенерированном байтовом коде. Он будет скопирован во все места, откуда вызывается функция.
  • Другой вариант использования - это reifiedпараметры типа, которые необходимо использовать inline. Прочтите здесь .
s1m0nw1
источник
5
технически вы все равно могли бы встроенные функции, которые не принимают лямбда-выражения, правильно? .. здесь преимущество в том, что в этом случае можно избежать накладных расходов на вызов функции .. язык, такой как Scala, позволяет это .. не уверен, почему Kotlin запрещает этот тип встроенных- ing
rogue-one
4
@ rogue-one Котлин на этот раз не запрещает встраивание. Авторы языка просто утверждают, что выигрыш в производительности, вероятно, будет незначительным. Небольшие методы, вероятно, уже будут встроены JVM во время JIT-оптимизации, особенно если они выполняются часто. Другой случай, когда это inlineможет быть вредно, - это когда функциональный параметр вызывается несколько раз во встроенной функции, например, в разных условных ветвях. Я только что столкнулся со случаем, когда из-за этого дублировался весь байт-код для функциональных аргументов.
Майк Хилл,
Когда нам следует использовать crossinlineпараметры?
Игорь Ганапольский
5

Самый важный случай, когда мы используем модификатор inline, - это когда мы определяем утилитарные функции с функциями параметров. Коллекция или обработка строк (например filter, mapили joinToString) или просто автономные функции являются прекрасным примером.

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

Если у нас нет параметра типа функции, параметра повторного типа и нам не нужен нелокальный возврат, то, скорее всего, нам не следует использовать встроенный модификатор. Вот почему у нас будет предупреждение в Android Studio или IDEA IntelliJ.

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

0xAliHn
источник
Что такое неместной возврат ?
Игорь Ганапольский
5

Функции высшего порядка очень полезны и действительно могут улучшить reusabilityкод. Однако одна из самых больших проблем при их использовании - это эффективность. Лямбда-выражения компилируются в классы (часто анонимные классы), а создание объектов в Java - сложная операция. Мы по-прежнему можем эффективно использовать функции высшего порядка, сохраняя при этом все преимущества, сделав функции встроенными.

вот встроенная функция в картинку

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

Вкратце: - Inline -> вместо того, чтобы быть вызванными, они заменяются кодом тела функции во время компиляции ...

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

Однако использование лямбда-выражений имеет некоторые недостатки. Поскольку это анонимные классы (и, следовательно, объекты), им нужна память (и они могут даже увеличивать общее количество методов вашего приложения). Чтобы этого избежать, мы можем встроить наши методы.

fun notInlined(getString: () -> String?) = println(getString())

inline fun inlined(getString: () -> String?) = println(getString())

Из приведенного выше примера : - Эти две функции делают одно и то же - выводят результат функции getString. Один встроен, а другой нет.

Если вы проверите декомпилированный java-код, вы увидите, что методы полностью идентичны. Это потому, что ключевое слово inline - это инструкция компилятору скопировать код на сайт вызова.

Однако, если мы передаем любой тип функции другой функции, как показано ниже:

//Compile time error… Illegal usage of inline function type ftOne...
 inline fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
 }

Чтобы решить эту проблему, мы можем переписать нашу функцию, как показано ниже:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

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

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

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

fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
}

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

Предположим, у нас есть такая функция ->

fun intercept() {
    // ...
    val start = SystemClock.elapsedRealtime()
    val result = doSomethingWeWantToMeasure()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    // ...}

Это работает нормально, но основа логики функции загрязнена кодом измерения, что затрудняет вашим коллегам работу над тем, что происходит.:)

Вот как встроенная функция может помочь этому коду:

 fun intercept() {
    // ...
    val result = measure { doSomethingWeWantToMeasure() }
    // ...
    }
 }

 inline fun <T> measure(action: () -> T) {
   val start = SystemClock.elapsedRealtime()
   val result = action()
   val duration = SystemClock.elapsedRealtime() - start
   log(duration)
   return result
 }

Теперь я могу сконцентрироваться на чтении основного предназначения функции intercept (), не пропуская строки кода измерения. Нам также выгодна возможность повторного использования этого кода в других местах, где мы хотим

inline позволяет вызывать функцию с лямбда-аргументом внутри замыкания ({...}) вместо передачи лямбда-подобной меры (myLamda)

Когда это полезно?

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

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

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

Хммм? Пример в android ->

Допустим, у нас есть функция в классе маршрутизатора активности для запуска действия и применения некоторых дополнительных функций.

fun startActivity(context: Context,
              activity: Class<*>,
              applyExtras: (intent: Intent) -> Unit) {
  val intent = Intent(context, activity)
  applyExtras(intent)
  context.startActivity(intent)
  }

Эта функция создает намерение, применяет некоторые дополнения, вызывая аргумент функции applyExtras, и запускает действие.

Если мы посмотрим на скомпилированный байт-код и декомпилируем его в Java, это будет выглядеть примерно так:

void startActivity(Context context,
               Class activity,
               Function1 applyExtras) {
  Intent intent = new Intent(context, activity);
  applyExtras.invoke(intent);
  context.startActivity(intent);
  }

Допустим, мы вызываем это из прослушивателя кликов в действии:

override fun onClick(v: View) {
router.startActivity(this, SomeActivity::class.java) { intent ->
intent.putExtra("key1", "value1")
intent.putExtra("key2", 5)
}
 }

Декомпилированный байт-код для этого прослушивателя кликов будет выглядеть примерно так:

@Override void onClick(View v) {
router.startActivity(this, SomeActivity.class, new Function1() {
@Override void invoke(Intent intent) {
  intent.putExtra("key1", "value1");
  intent.putExtra("key2", 5);
}
 }
}

Новый экземпляр Function1 создается каждый раз, когда запускается прослушиватель кликов. Это нормально работает, но не идеально!

Теперь давайте просто добавим inline к нашему методу маршрутизатора активности:

inline fun startActivity(context: Context,
                     activity: Class<*>,
                     applyExtras: (intent: Intent) -> Unit) {
 val intent = Intent(context, activity)
 applyExtras(intent)
 context.startActivity(intent)
 }

Не меняя вообще код прослушивателя кликов, теперь мы можем избежать создания этого экземпляра Function1. Эквивалент Java-кода прослушивателя кликов теперь будет выглядеть примерно так:

@Override void onClick(View v) {
Intent intent = new Intent(context, SomeActivity.class);
intent.putExtra("key1", "value1");
intent.putExtra("key2", 5);
context.startActivity(intent);
}

Это оно.. :)

«Встроить» функцию в основном означает скопировать тело функции и вставить его в место вызова функции. Это происходит во время компиляции.

Вини
источник
Я не вижу пользы от использования inline functions. Можете ли вы дать реальный пример использования (например, в реальном проекте, над которым вы работали)?
Игорь Ганапольский
1
@IgorGanapolsky добавил пример с Android, посмотрите
Вини
3

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

fun timer(block: () -> Unit) {
    // stuff
    block()
    //stuff
}

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

    // This is an error
    timer { asyncLogic() }
}

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

suspend fun timer(block: suspend () -> Unit) {
    // stuff
    block()
    // stuff
}

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

inline fun timer(block: () -> Unit) {
    // stuff
    block()
    // stuff
}

fun main() {
    // timer can be used from anywhere now
    timer { logic() }

    launch {
        timer { asyncLogic() }
    }
}

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

Энтони Наддео
источник
Ваш фрагмент kotlin детская площадка не использует inline fun. Не могли бы вы уточнить?
Игорь Ганапольский
@IgorGanapolsky Снова просмотрите последнюю часть моего поста. Я говорю вам, какой из них должен быть встроенным, и у меня также есть пример в ответе выше.
Энтони Наддео,