何时在 Kotlin 中使用内联函数



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

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();
}
}

假设您创建了一个高阶函数,该函数采用类型() -> Unit(无参数,无返回值)的 lambda,并按如下所示执行它:

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");

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

让我补充一下:何时不使用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.在这里阅读。

使用inline防止对象创建

Lambda 被转换为类

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

fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}

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

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

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

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

fun doSomething() {
println("Before lambda")
doSomethingElse {
println("Inside lambda")
}
println("After lambda")
}

问题:对象

编译器将 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");
}

这里的问题是,如果你在一个循环中调用这个函数数千次,就会创建数千个对象并被垃圾回收。这会影响性能。

解决方案:inline

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

inline fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}

这会导致在调用站点复制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");
}

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


使用inline防止变量捕获

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

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

如果我们这里的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");
}
});
}

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

因此,正如您从上面的两个示例中所看到的,当函数将其他函数作为参数时,inline函数的大量性能优势是可以实现的。这是inline功能最有益且最值得使用的时候。无需inline其他常规函数,因为 JIT 编译器已经在必要时将它们内联在后台

。<小时 />

使用inline更好地控制流量

由于非内联函数类型被转换为类,因此我们无法在 lambda 中编写return语句:

fun doSomething() {
doSomethingElse {
return    // Error: return is not allowed here
}
}

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


inline用于reified类型参数

在 Kotlin 中使用泛型时,我们可以使用类型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
}

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

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
}

内联会导致复制实际类型参数来代替T。因此,例如,当您像doSomething("Some String")一样调用函数时,T::class.simpleName会变得String::class.simpleNamereified关键字只能与inline函数一起使用。


避免在重复通话时inline

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

inline fun doSomething() {
println("Doing something")
}

第一个抽象级别

inline fun doSomethingAgain() {
doSomething()
doSomething()
}

结果:

public static final void doSomethingAgain() {
System.out.println("Doing something");
System.out.println("Doing something");
}

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

第二个抽象级别

inline fun doSomethingAgainAndAgain() {
doSomethingAgain()
doSomethingAgain()
}

结果:

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");
}

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

第三个抽象级别

inline fun doSomethingAgainAndAgainAndAgain() {
doSomethingAgainAndAgain()
doSomethingAgainAndAgain()
}

结果:

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");
}

在第三个抽象级别,代码增长到:23 =8 行。

同样,在第四个抽象级别,代码以 24= 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() }

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


隐藏实现时无法使用inline

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

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

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


检查生成的代码

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

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

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

fun main() {
lock { println("Inside the block()") }
}

生成的类

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

final class your/package/YourFilenameKt$main$1 extends Lambda implements Function0 { }

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

生成的对象

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

GETSTATIC your/package/YourFilenameKt$main$1.INSTANCE

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

<小时 />

就是这样!希望能为inline功能提供有用的见解。

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

这是内联功能进入图片

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

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

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

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

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

从上面的例子中:- 这两个函数做完全相同的事情 - 打印getString函数的结果。一个是内联的,一个不是。

如果你检查反编译的java代码,你会发现这些方法完全一样。这是因为内联关键字是编译器将代码复制到调用站点的指令。

但是,如果我们将任何函数类型传递给另一个函数,如下所示:

//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 参数调用函数,而不是传递类似 lambda 的度量(myLamda)

这什么时候有用?

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

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

使用函数上的内联关键字,编译时转换永远不会发生。相反,内联函数的主体插入到其调用站点,并且执行其代码,而无需创建函数实例的开销。

嗯?安卓中的例子 -->

假设我们在活动路由器类中有一个函数来启动活动并应用一些额外内容

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

此函数创建一个 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 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);
}

就是这样。 :)

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

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

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

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

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

您可能想要一个的简单情况是,当您创建一个接受挂起块的 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
}

但是它只能从协程/挂起函数本身使用。然后,您最终将创建这些实用程序的异步版本和非异步版本。如果您将其内联,问题就会消失。

inline fun timer(block: () -> Unit) {
// stuff
block()
// stuff
}
fun main() {
// timer can be used from anywhere now
timer { logic() }
launch {
timer { asyncLogic() }
}
}

这是一个具有错误状态的 kotlin 游乐场。使计时器内联以解决它。

fun higherOrder(lambda:():Unit){
//invoking lambda
lambda()
}
//Normal function calling higher-order without inline
fun callingHigerOrder() {
higherOrder()
//Here an object will be created for the lambda inside the higher-order function 
}
//Normal function calling higher-order with inline
fun callingHigerOrder() {
higherOrder()
//Here there will be no object created and the contents of the lambda will be called directly into this calling function. 
}

如果要避免在调用端创建对象,请使用内联。 因此,当使用内联时,正如我们所理解的那样,lambda 将是调用函数的一部分,如果 lambda 块内有返回调用,那么整个调用函数将被返回,这称为非本地返回。 为避免非本地返回,请在高阶函数的 lambda 块之前使用交叉内联。

相关内容

  • 没有找到相关文章

最新更新