【问题标题】:when to use an inline function in Kotlin?何时在 Kotlin 中使用内联函数?
【发布时间】:2017-11-12 06:25:56
【问题描述】:

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

lock(l) { foo() }

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

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

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

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

【问题讨论】:

  • 这有两个主要用例,一个是具有某些类型的高阶函数,另一个是具体类型参数。内联函数的文档包括:kotlinlang.org/docs/reference/inline-functions.html
  • @zsmb13 谢谢,先生。但我不明白:“编译器可以发出以下代码,而不是为参数创建函数对象并生成调用”
  • 我也没有得到那个例子。
  • 是什么让您说inline 函数可以提高性能,并且用户会注意到它?

标签: function kotlin inline-functions


【解决方案1】:

假设您创建了一个接受 () -> 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");

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

【讨论】:

  • 首先使用 Function 对象包装器有什么好处?即 - 为什么不是所有内容都内联?
  • 这样你也可以任意将函数作为参数传递,存储在变量中等等。
  • 你可以,如果你用它们做复杂的事情,你最终会想知道noinlinecrossinline关键字 - 请参阅docs
  • 文档给出了默认情况下您不想内联的原因:内联可能会导致生成的代码增长;但是,如果我们以合理的方式(即避免内联大型函数)这样做,它将在性能上得到回报,尤其是在循环内的“超多态”调用站点。
  • 但是您没有回答“何时使用”的问题。你刚刚解释了它是如何使用的。答案在文档中,并指出如果您有一个包含少量代码的函数并且该函数被大量使用,那么使用内联函数可以提高性能。
【解决方案2】:

让我补充一下:什么时候不使用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。阅读here

【讨论】:

  • 从技术上讲,您仍然可以内联不采用 lambda 表达式的函数吗?...这里的优点是在这种情况下避免了函数调用开销.. 像 Scala 这样的语言允许这样做.. 不确定为什么 Kotlin 禁止这种类型的内联
  • @rogue-one Kotlin 并没有禁止这次内联。语言作者只是声称性能优势可能微不足道。在 JIT 优化期间,JVM 可能已经内联了小方法,尤其是在它们被频繁执行的情况下。 inline 可能有害的另一种情况是在内联函数中多次调用函数参数时,例如在不同的条件分支中。我刚刚遇到了一个案例,因为这个原因,函数参数的所有字节码都被复制了。
  • 什么时候应该使用crossinline参数?
【解决方案3】:

使用inline 防止对象创建

Lambda 被转换为类

在 Kotlin/JVM 中,函数类型 (lambda) 被转换为扩展接口 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 函数提供有用的见解。

【讨论】:

    【解决方案4】:

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

    内联函数出现在图片中

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

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

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

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

    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

    什么时候有用?

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

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

    使用函数上的 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 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 的优势。你能给出一个真实世界的用例(即在你从事的真实项目中)吗?
    • @IgorGanapolsky 添加了 android 示例看看
    【解决方案5】:

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

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

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

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

    【讨论】:

    • 什么是非本地返回
    【解决方案6】:

    当您创建一个接收挂起块的 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 playground。使计时器内联来解决它。

    【讨论】:

    • 你的 kotlin playground sn-p 不使用inline fun。你能澄清一下吗?
    • @IgorGanapolsky 再次查看我帖子的最后一部分。我告诉你哪一个应该是内联的,我在上面的答案中也有例子。
    【解决方案7】:
    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 块之前使用交叉内联。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-03
      • 1970-01-01
      • 1970-01-01
      • 2023-03-23
      相关资源
      最近更新 更多