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)
在这种情况下,不会创建新实例.
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
。在这里阅读。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
语句会被复制到调用函数中。
inline
的reified
类型参数在 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
函数的有用见解。
当我们使用 inline 修饰符时,最重要的情况是当我们使用参数函数定义类似 util 的函数时。集合或字符串处理(如filter
,map
或joinToString
)或只是独立函数是一个完美的例子。
这就是为什么内联修饰符主要是库开发人员的重要优化。他们应该知道它是如何工作的,以及它的改进和成本是什么。当我们使用函数类型参数定义我们自己的 util 函数时,我们应该在我们的项目中使用 inline 修饰符。
如果我们没有函数类型参数、具体化类型参数,并且我们不需要非本地返回,那么我们很可能不应该使用内联修饰符。这就是为什么我们会在 Android Studio 或 IDEA IntelliJ 上发出警告。
此外,还有代码大小问题。内联大型函数可能会显着增加字节码的大小,因为它会被复制到每个调用站点。在这种情况下,您可以重构函数并将代码提取为常规函数。
归档时间: |
|
查看次数: |
18605 次 |
最近记录: |