什么时候在Kotlin中使用内联函数?


105

我知道内联函数可能会提高性能并导致生成的代码增长,但是我不确定何时使用它是正确的。

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 2015年

2
@ zsmb13谢谢,先生。但我不明白:“编译器可能会生成以下代码,而不是为参数创建函数对象并生成调用”:
holi-java

2
我也没有那个例子。
filthy_wizard

Answers:


279

假设您创建了一个高阶函数,该函数采用类型为lambda () -> 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");
}

当您从Kotlin拨打电话时...

nonInlined {
    println("do something here")
}

在幕后,Function将在此处创建一个实例,该实例将代码包装在lambda中(再次简化):

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

因此,基本上,调用此函数并将lambda传递给该函数将始终创建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");

在这种情况下,不会创建新实例。


19
首先使用Function对象包装器有什么好处?即-为什么不内联所有内容?
Arturs Vancans

14
这样,您还可以任意地传递函数作为参数,将它们存储在变量中,等等
。– zsmb13

6
@ zsmb13的
出色

2
可以,而且如果您对它们进行复杂的处理,最终您将想了解noinlineand crossinline关键字-请参阅docs
zsmb13

2
文档提供了一个默认情况下您不想内联的原因
CorayThan

43

让我补充一下:“何时不使用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没有包装在一个对象中(这是隐含的,inline因为您要避免这种情况),那么如何将其完全分配给变量?在这种情况下,编译器建议设置参数noinline。具有一个inline函数和一个noinline函数没有任何意义,不要那样做。但是,如果功能类型有多个参数,请在需要时考虑内联其中一些参数。

所以这是一些建议的规则:

  • 当直接调用所有功能类型参数或将其传递给其他内联函数时,您可以
  • 如果是^,则应内联。
  • 将函数参数分配给函数内的变量时,无法内联
  • 应该考虑内联,如果可以内联至少一个函数类型参数,noinline则将其用于其他参数。
  • 不应该内联巨大的函数,请考虑生成的字节码。它将被复制到调用该函数的所有位置。
  • 另一个用例是reified类型参数,需要使用inline在这里阅读。

4
从技术上讲,您仍然可以内联不使用lambda表达式的函数吗?..这里的好处是在那种情况下避免了函数调用开销..像Scala这样的语言允许这样做..不知道为什么Kotlin禁止这种内联- ing
流氓一号

3
@ rogue-one Kotlin这次没有禁止内联。语言作者只是声称性能收益可能微不足道。在JIT优化期间,JVM可能已经内联了小型方法,特别是如果它们频繁执行时。另一种inline可能有害的情况是在内联函数中(例如在不同的条件分支中)多次调用功能参数时。我只是碰到了这样一个情况,所有函数参数的字节码都被复制了。
Mike Hill

5

当使用内联修饰符时,最重要的情况是使用参数函数定义类似util的函数。集合或字符串处理(如filtermapjoinToString)或只是独立的功能就是一个很好的例子。

这就是为什么内联修饰符对于库开发人员而言主要是重要的优化的原因。他们应该知道它的工作原理以及它的改进和成本。当我们使用函数类型参数定义自己的util函数时,应该在项目中使用inline修饰符。

如果我们没有函数类型参数,修饰类型参数,并且不需要非本地返回值,那么我们很可能不应该使用内联修饰符。这就是为什么我们会在Android Studio或IDEA IntelliJ上发出警告的原因。

此外,还有代码大小问题。内联大型函数可能会大大增加字节码的大小,因为它已复制到每个调用站点。在这种情况下,您可以重构函数并将代码提取为常规函数。


4

高阶函数非常有帮助,它们可以真正改善reusability代码。但是,使用它们的最大问题之一是效率。Lambda表达式被编译为类(通常是匿名类),而Java中的对象创建是一项繁重的操作。通过使函数内联,我们仍然可以有效地使用高阶函数,同时保留所有好处。

这是内联函数的图片

将功能标记为时inline,在代码编译期间,编译器将使用该功能的实际主体替换所有功能调用。同样,作为参数提供的lambda表达式将替换为其实际主体。它们不会被视为函数,而是实际的代码。

简而言之:-内联->而不是被调用,而是在编译时由函数的主体代码代替...

在Kotlin中,使用一个函数作为另一个函数(所谓的高阶函数)的参数比使用Java更自然。

但是,使用lambda有一些缺点。由于它们是匿名类(因此是对象),因此它们需要内存(甚至可能增加您应用程序的总体方法数量)。为了避免这种情况,我们可以内联我们的方法。

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)
    /*...*/}

在这里,当只有一个lambda参数并且我们将其传递给另一个函数时,编译器将告诉我们不要使用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()函数的主要意图,而无需跳过测量代码行。我们还受益于可以在其他地方重用该代码的选项

内联允许您在闭包({...})中调用带有lambda参数的函数,而不是像measure(myLamda)一样传递lambda


2

您可能想要的一种简单情况是,创建一个带有暂停块的util函数。考虑一下。

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
}

但是,它只能在协程/挂起函数本身中使用。然后,您将最终制作这些utils的异步版本和非异步版本。如果将其内联,问题将消失。

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

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

    launch {
        timer { asyncLogic() }
    }
}

这是状态错误的科特林游乐场。使计时器内联解决。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.