何时在Kotlin中使用内联函数?

hol*_*ava 87 function inline-functions kotlin

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

lock(l) { foo() }
Run Code Online (Sandbox Code Playgroud)

编译器可以发出以下代码,而不是为参数创建函数对象并生成调用.(来源)

l.lock()
try {
  foo()
}
finally {
  l.unlock()
}
Run Code Online (Sandbox Code Playgroud)

但我发现kotlin没有为非内联函数创建的函数对象.为什么?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}
Run Code Online (Sandbox Code Playgroud)

zsm*_*b13 231

假设你创建一个更高阶函数,它接受一个lambda类型() -> Unit(没有参数,没有返回值),并执行它,如下所示:

fun nonInlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}
Run Code Online (Sandbox Code Playgroud)

用Java的说法,这将转化为类似的东西(简化!):

public void nonInlined(Function block) {
    System.out.println("before");
    block.invoke();
    System.out.println("after");
}
Run Code Online (Sandbox Code Playgroud)

当你从Kotlin打电话时......

nonInlined {
    println("do something here")
}
Run Code Online (Sandbox Code Playgroud)

在引擎盖下,Function将在这里创建一个实例,它将代码包装在lambda中(同样,这是简化的):

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

所以基本上,调用此函数并将lambda传递给它将始终创建一个Function对象的实例.


另一方面,如果您使用inline关键字:

inline fun inlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}
Run Code Online (Sandbox Code Playgroud)

当你这样称呼时:

inlined {
    println("do something here")
}
Run Code Online (Sandbox Code Playgroud)

不会Function创建任何实例,相反,block内联函数内部调用的代码将被复制到调用站点,因此您将在字节码中得到类似的内容:

System.out.println("before");
System.out.println("do something here");
System.out.println("after");
Run Code Online (Sandbox Code Playgroud)

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

  • 首先使用Function对象包装器有什么好处?即 - 为什么不是一切内联? (16认同)
  • 这样你也可以任意传递函数作为参数,将它们存储在变量中等. (11认同)
  • @ zsmb13的精彩解释 (3认同)
  • 文档给出了默认情况下您不想内联的原因:_Inlining 可能会导致生成的代码增长;然而,如果我们以合理的方式(即避免内联大型函数),它会在性能上有所回报,尤其是在循环内的“巨态”调用点。_ (3认同)
  • 但是您没有回答“何时使用”这个问题。你刚刚解释了它是如何使用的。答案在文档中,并指出如果您有一个包含少量代码的函数并且该函数被大量使用,那么使用内联函数可以提高性能。 (3认同)
  • 可以,而且如果您对它们进行复杂的处理,您最终将想知道`noinline`和`crossinline`关键字-请参阅[docs](https://kotlinlang.org/docs/reference/inline-functions .html)。 (2认同)
  • 令人敬畏的解释zsmb13.我检查了几个地方,但无法正确理解.谢谢你分享这个...... (2认同)
  • 您能否发布一个示例,展示如何将具有两个 lambda 函数输入参数(其中一个用 noinline 修饰符注释)的内联函数转换为.. (2认同)

s1m*_*nw1 22

让我添加:“何时不使用inline

1)如果您有一个简单的函数不接受其他函数作为参数,则内联它们是没有意义的。IntelliJ将警告您:

内联'...'的预期性能影响微不足道。内联最适合具有功能类型参数的功能

2)即使您有一个“带有函数类型参数的函数”,您也可能会遇到编译器告诉您内联不起作用的情况。考虑以下示例:

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

这段代码不会与错误一起编译:

'...'中非法使用内联参数'operation'。在参数声明中添加“ noinline”修饰符。

原因是编译器无法内联此代码。如果operation没有包装在一个对象中(这是隐含的,inline因为您要避免这种情况),那么如何将其完全分配给变量?在这种情况下,编译器建议设置参数noinline。具有一个inline函数和一个noinline函数没有任何意义,不要那样做。但是,如果功能类型有多个参数,请根据需要考虑内联其中一些参数。

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

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

  • @rogue-one Kotlin 并不禁止这次内联。语言作者只是声称性能优势可能微不足道。小方法很可能在 JIT 优化期间被 JVM 内联,特别是如果它们被频繁执行的话。另一种“内联”可能有害的情况是在内联函数中多次调用函数参数,例如在不同的条件分支中。我刚刚遇到了一种情况,由于这个原因,函数参数的所有字节码都被复制了。 (8认同)
  • 从技术上讲,你仍然可以内联不接受 lambda 表达式的函数?..这里的优点是在这种情况下避免了函数调用开销..像 Scala 这样的语言允许这样做..不知道为什么 Kotlin 禁止这种类型的内联-英 (6认同)
  • 我们什么时候应该使用“crossinline”参数? (2认同)

Win*_*ini 20

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

这里是内联函数的图片

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

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

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

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

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

inline fun inlined(getString: () -> String?) = println(getString())
Run Code Online (Sandbox Code Playgroud)

从上面的例子来看:- 这两个函数做的完全一样——打印 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)
    /*...*/
 }
Run Code Online (Sandbox Code Playgroud)

为了解决这个问题,我们可以重写我们的函数如下:

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)
    /*...*/}
Run Code Online (Sandbox Code Playgroud)

假设我们有一个高阶函数,如下所示:

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

在这里,当只有一个 lambda 参数并且我们将它传递给另一个函数时,编译器会告诉我们不要使用 inline 关键字。所以,我们可以将上面的函数改写如下:

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

注意:-我们还必须删除关键字 noinline,因为它只能用于内联函数!

假设我们有这样的函数-->

fun intercept() {
    // ...
    val start = SystemClock.elapsedRealtime()
    val result = doSomethingWeWantToMeasure()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    // ...}
Run Code Online (Sandbox Code Playgroud)

这工作正常,但函数逻辑的核心被测量代码污染,使您的同事更难处理正在发生的事情。:)

以下是内联函数如何帮助此代码:

 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
 }
Run Code Online (Sandbox Code Playgroud)

现在我可以专注于阅读intercept() 函数的主要意图,而无需跳过测量代码行。我们还可以从在我们想要的其他地方重用该代码的选项中受益

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

这什么时候有用?

inline 关键字对于接受其他函数或 lambdas 作为参数的函数很有用。

如果函数上没有 inline 关键字,则该函数的 lambda 参数会在编译时转换为具有名为 invoke() 的单个方法的 Function 接口的实例,并且通过对该 Function 实例调用 invoke() 来执行 lambda 中的代码在函数体内。

使用函数上的 inline 关键字,编译时转换永远不会发生。相反,内联函数的主体被插入到它的调用点,并且它的代码在没有创建函数实例的开销的情况下被执行。

嗯?android中的例子-->

假设我们在活动路由器类中有一个函数来启动活动并应用一些附加功能

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

此函数创建一个意图,通过调用 applyExtras 函数参数应用一些附加功能,并启动活动。

如果我们查看编译后的字节码并将其反编译为 Java,则如下所示:

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

假设我们从活动中的点击侦听器调用它:

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

这个点击监听器的反编译字节码看起来像这样:

@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);
}
 }
}
Run Code Online (Sandbox Code Playgroud)

每次触发单击侦听器时,都会创建一个 Function1 的新实例。这工作正常,但并不理想!

现在让我们将内联添加到我们的活动路由器方法中:

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

根本不更改我们的点击侦听器代码,我们现在可以避免创建该 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);
}
Run Code Online (Sandbox Code Playgroud)

就是这样.. :)

“内联”函数基本上意味着复制函数的主体并将其粘贴到函数的调用站点。这发生在编译时。


Yog*_*ity 14

使用inline预防对象创建

Lambda 转换为类

在 Kotlin/JVM 中,函数类型 (lambdas) 被转换为扩展接口的匿名/常规类Function。考虑以下函数:

fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}
Run Code Online (Sandbox Code Playgroud)

上面的函数,编译后如下所示:

public static final void doSomethingElse(Function0 lambda) {
    System.out.println("Doing something else");
    lambda.invoke();
}
Run Code Online (Sandbox Code Playgroud)

函数类型() -> Unit转换为接口Function0

现在让我们看看当我们从其他函数调用这个函数时会发生什么:

fun doSomething() {
    println("Before lambda")
    doSomethingElse {
        println("Inside lambda")
    }
    println("After lambda")
}
Run Code Online (Sandbox Code Playgroud)

问题:对象

编译器将 lambda 替换为匿名Function类型的对象:

public static final void doSomething() {
    System.out.println("Before lambda");
    doSomethingElse(new Function() {
            public final void invoke() {
            System.out.println("Inside lambda");
        }
    });
    System.out.println("After lambda");
}
Run Code Online (Sandbox Code Playgroud)

这里的问题是,如果您在循环中调用此函数数千次,将创建数千个对象并进行垃圾回收。这会影响性能。

解决方案: inline

通过inline在函数前添加关键字,我们可以告诉编译器在调用站点复制该函数的代码,而无需创建对象

inline fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}
Run Code Online (Sandbox Code Playgroud)

这导致复制inline函数的代码以及lambda()调用站点的代码:

public static final void doSomething() {
    System.out.println("Before lambda");
    System.out.println("Doing something else");
    System.out.println("Inside lambda");
    System.out.println("After lambda");
}
Run Code Online (Sandbox Code Playgroud)

如果将有/无inline关键字与for循环中的一百万次重复进行比较,这会使执行速度加倍。因此,将其他函数作为参数的函数在内联时会更快。


使用inline防止变量捕获

当你在 lambda 内部使用局部变量时,它被称为变量捕获(闭包):

fun doSomething() {
    val greetings = "Hello"                // Local variable
    doSomethingElse {
        println("$greetings from lambda")  // Variable capture
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我们doSomethingElse()这里的函数不是inline,则在创建我们之前看到的匿名对象时,通过构造函数将捕获的变量传递给 lambda:

public static final void doSomething() {
    String greetings = "Hello";
    doSomethingElse(new Function(greetings) {
            public final void invoke() {
            System.out.println(this.$greetings + " from lambda");
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

如果在 lambda 内部使用了许多局部变量或在循环中调用 lambda,则通过构造函数传递每个局部变量会导致额外的内存开销。inline在这种情况下使用该函数有很大帮助,因为该变量直接在调用站点使用。

因此,从上面的两个示例中可以看出,inline当函数将其他函数作为参数时,函数的大部分性能优势就实现了。这是inline功能最有用和最值得使用的时候。不需要inline其他通用函数,因为 JIT 编译器已经在必要时将它们内联在幕后。


使用inline更好的控制流

由于非内联函数类型转换为类,我们不能return在 lambda 内部编写语句:

fun doSomething() {
    doSomethingElse {
        return    // Error: return is not allowed here
    }
}
Run Code Online (Sandbox Code Playgroud)

这被称为非本地,return因为它不是调用函数的本地doSomething()。不允许非本地的原因return是该return语句存在于另一个类中(在前面显示的匿名类中)。制作doSomethingElse()函数inline解决了这个问题,我们可以使用非本地返回,因为这样return语句会被复制到调用函数中。


使用inlinereified类型参数

在 Kotlin 中使用泛型时,我们可以使用 type 的值T。但是我们不能直接使用类型,我们得到错误Cannot use 'T' as reified type parameter. Use a class instead

fun <T> doSomething(someValue: T) {
    println("Doing something with value: $someValue")               // OK
    println("Doing something with type: ${T::class.simpleName}")    // Error
}
Run Code Online (Sandbox Code Playgroud)

这是因为我们传递给函数的类型参数在运行时被删除了。因此,我们不可能确切地知道我们正在处理哪种类型。

使用inline函数和reified类型参数可以解决这个问题:

inline fun <reified T> doSomething(someValue: T) {
    println("Doing something with value: $someValue")               // OK
    println("Doing something with type: ${T::class.simpleName}")    // OK
}
Run Code Online (Sandbox Code Playgroud)

内联导致实际类型参数被复制而不是T. 因此,例如,在T::class.simpleName成为String::class.simpleName,当你调用的功能等doSomething("Some String")。该reified关键字只能被使用inline的功能。


避免inline呼叫重复时

假设我们有以下在不同抽象级别重复调用的函数:

inline fun doSomething() {
    println("Doing something")
}
Run Code Online (Sandbox Code Playgroud)

第一抽象层

inline fun doSomethingAgain() {
    doSomething()
    doSomething()
}
Run Code Online (Sandbox Code Playgroud)

结果是:

public static final void doSomethingAgain() {
    System.out.println("Doing something");
    System.out.println("Doing something");
}
Run Code Online (Sandbox Code Playgroud)

在第一个抽象级别,代码增长为:2 1 = 2 行。

第二个抽象层次

inline fun doSomethingAgainAndAgain() {
    doSomethingAgain()
    doSomethingAgain()
}
Run Code Online (Sandbox Code Playgroud)

结果是:

public static final void doSomethingAgainAndAgain() {
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
}
Run Code Online (Sandbox Code Playgroud)

在第二个抽象级别,代码增长为:2 2 = 4 行。

第三个抽象层次

inline fun doSomethingAgainAndAgainAndAgain() {
    doSomethingAgainAndAgain()
    doSomethingAgainAndAgain()
}
Run Code Online (Sandbox Code Playgroud)

结果是:

public static final void doSomethingAgainAndAgainAndAgain() {
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
}
Run Code Online (Sandbox Code Playgroud)

在第三个抽象级别,代码增长为:2 3 = 8 行。

类似地,在第四个抽象级别,代码增长为 2 4 = 16 行,依此类推。

数字 2 是函数在每个抽象级别被调用的次数。正如你所看到的,代码不仅在最后一层而且在每一层都呈指数增长,所以这是 16 + 8 + 4 + 2 行。为了保持简洁,我在这里只展示了 2 个调用和 3 个抽象级别,但想象一下将为更多调用和更多抽象级别生成多少代码。这会增加您的应用程序的大小。这是为什么你不应该inline在你的应用程序中使用每一个功能的另一个原因。


避免inline在递归循环中

避免将inline函数用于函数调用的递归循环,如以下代码所示:

// Don't use inline for such recursive cycles

inline fun doFirstThing() { doSecondThing() }
inline fun doSecondThing() { doThirdThing() }
inline fun doThirdThing() { doFirstThing() }
Run Code Online (Sandbox Code Playgroud)

这将导致函数复制代码的循环永无止境。编译器给你一个错误:The 'yourFunction()' invocation is a part of inline cycle


inline隐藏实现时不能使用

公共inline函数不能访问private函数,因此它们不能用于实现隐藏:

inline fun doSomething() {
    doItPrivately()  // Error
}

private fun doItPrivately() { }
Run Code Online (Sandbox Code Playgroud)

inline上面显示的函数中,访问该private函数doItPrivately()会出现错误:Public-API inline function cannot access non-public API fun


检查生成的代码

现在,关于你问题的第二部分:

但是我发现 kotlin 没有为非内联函数创建的函数对象。为什么?

Function物体的确是创建。要查看创建的Function对象,您需要在lock()函数内部实际调用您的main()函数,如下所示:

fun main() {
    lock { println("Inside the block()") }
}
Run Code Online (Sandbox Code Playgroud)

生成类

生成的Function类不会反映在反编译的 Java 代码中。您需要直接查看字节码。查找以以下开头的行:

final class your/package/YourFilenameKt$main$1 extends Lambda implements Function0 { }
Run Code Online (Sandbox Code Playgroud)

这是编译器为传递给lock()函数的函数类型生成的类。该main$1是你创建的类的名称block()功能。有时该类是匿名的,如第一部分中的示例所示。

生成对象

在字节码中,查找以以下开头的行:

GETSTATIC your/package/YourFilenameKt$main$1.INSTANCE
Run Code Online (Sandbox Code Playgroud)

INSTANCE是为上述类创建的对象。创建的对象是一个单例,因此名称为INSTANCE


就是这样!希望提供对inline函数的有用见解。

  • 该死,非常好的答案 (9认同)
  • 这是我期望官方文档达到的清晰标准。 (6认同)

0xA*_*iHn 5

当我们使用 inline 修饰符时,最重要的情况是当我们使用参数函数定义类似 util 的函数时。集合或字符串处理(如filter,mapjoinToString)或只是独立函数是一个完美的例子。

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

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

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