Skip to content

Latest commit

 

History

History
329 lines (271 loc) · 9.39 KB

17-inline.md

File metadata and controls

329 lines (271 loc) · 9.39 KB

Kotlin中的inline, noinline, crossinline, reified

  • Kotlin中的inline, noinline, crossinline都是什么意思? 干什么用的?
  • Kotlin中的reified又是干什么用的?

本篇文章介绍Kotlin的inline函数, 顺一顺相关的知识点, 解决这些问题.

inline: 内联

最开始接触inline这个词是学C/C++的时候, 叫内联. 编译器会把函数体替换在函数被调用的地方.

Kotlin中的inline也是这个意思, 主要是解决了函数调用时的开销, 调用栈的保存, 匿名对象的建立等. 因为Kotlin支持高阶函数, lambda等, 所以用inline帮助降低一些运行时开销.

inline让编译器直接把代码复制到调用的地方, 比起直接复制粘贴代码, 同时又保持了函数的复用性和可读性.

Java在语言层面暂时不支持inline, JVM会做一些相关的优化.

inline做什么

举个例子来看看inline和不inline的代码有什么区别:

fun main() {
    sayHi {
        println("I'm wind, what's your name?")
    }
}

fun sayHi(body: () -> Unit) {
    println("Hi, ")
    body()
    println("Bye!")
}

decompile后的Java代码:

public static final void main() {
  sayHi((Function0)null.INSTANCE);
}

public static final void sayHi(@NotNull Function0 body) {
  Intrinsics.checkParameterIsNotNull(body, "body");
  String var1 = "Hi, ";
  boolean var2 = false;
  System.out.println(var1);
  body.invoke();
  var1 = "Bye!";
  var2 = false;
  System.out.println(var1);
}

main调用了sayHi, sayHi里面又执行了body.

如果只改动一行, 给sayHi方法加上inline关键字:

inline fun sayHi(body: () -> Unit) {
    println("Hi, ")
    body()
    println("Bye!")
}

那么decompile后:

public static final void main() {
	int $i$f$sayHi = false;
	String var1 = "Hi, ";
	boolean var2 = false;
	System.out.println(var1);
	int var3 = false;
	String var4 = "I'm wind, what's your name?";
	boolean var5 = false;
	System.out.println(var4);
	var1 = "Bye!";
	var2 = false;
	System.out.println(var1);
}

可以看到inline之后, main中的代码就是实际做事情的代码, 它不知道自己调用了sayHi, 也没有为lambda参数body建立对象.

如果这个方法是在循环中调用的, 加个inline关键字可以省下不少对象的建立.

inline修饰符同时作用于函数本身和它的函数类型参数: 它们都被inline到被调用的地方了.

什么时候使用inline

如果你在一个很简单的非高阶函数前面加上inline, 举个例子:

inline fun sayName() {
    println("Wind")
}

那么你会遇到IDE把inline标黄, 并且提示:

Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types.

因为这样做没有什么必要了.

inline的适用场景: 高阶工具类函数. 比如filter, map, joinToString, repeat等.

inline不适用于:

  • 很大很长的函数.

noinline

noinline又是用来干啥呢?

前面说过inline同时作用于函数本身和它的函数(lambda)参数. 如果函数有多个函数参数, 有些我不希望被inline, 那就可以用noinline来修饰.

inline fun aMixedInlineFunction(inlined: () -> Unit, noinline notInlined: () -> Unit) {
    inlined()
    notInlined()
}

crossinline

还是按上面的思路, 先看看crossinline出现的原因.

首先复习一下return的相关知识点.

local return

  • 一般情况下, 方法里面的lambda是不能return外部函数的.

举例: 这是个普通的高阶方法, 带有一个lambda参数:

fun fooNormal(body: () -> Unit) {
    println("normal start")
    body()
    println("normal done")
}

它被调用的时候, 如果想在lambda中直接return:

fun main() {
    fooNormal {
        println("body 1")
        return // return is not allowed here
        return@fooNormal // return@fooNormal is allowed
    }
}

return会被标红, 提示return is not allowed here.

只能带上一个label写return@fooNormal, 表示只是退出当前这个lambda, 而不是退出外面的函数.

这种叫做local return, 因为只退出了最近的闭包. lambda闭包之外, 函数后面的语句还是会照常执行.

non-local return

  • inline方法里面的lambda可以return外部函数.

把上面的例子稍微改一下, 把方法改成inline的:

inline fun fooInline(body: () -> Unit) {
    println("inline start")
    body()
    println("inline done")
}

调用的时候:

fun main() {
    fooInline {
        println("body 2")
        return
    }

    println("the end of main")
}

这时候就可以在lambda里面直接写return了.

运行结果:

inline start
body 2

可以看到不仅fooInline方法后面的语句没有被执行, 连main都退出了. 联想一下inline的原理, 很好理解.

在这种情况下, lambda中的return实际上是作用于方法的调用处的. 这就是著名的non-local return.

很多集合的方法都是inline的,

这就是为什么在forEach中可以直接用return从方法中跳出来:

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // returns from hasZeros
    }
    return false
}

crossinline : disable non-local return

但是有时候作为参数传入的lambda不一定是被函数直接使用, 有可能会被嵌套.

在这种情况下, 规范干脆规定禁止了non-local return, 否则容易写出混乱的代码.

比如这个方法:

inline fun fooWithCrossinline2(body: () -> Unit) {
    val f = Runnable { body() } // Error
    println("fooWithCrossinline 2")
}

这样写直接就报错了:

Can't inline `body` here: it may contain non-local returns. Add `crossinline` modifier to parameter declaration `body`

这个提示明明白白, 此时按下Alt+Enter, 给参数加上crossinline即修好:

inline fun fooWithCrossinline2(crossinline body: () -> Unit) {
    val f = Runnable { body() }
    println("fooWithCrossinline 2")
}

调用这个方法的时候, 如果在lambda中企图进行non-local return, 会和普通方法一样提示不行:

fooWithCrossinline2 { 
    return // Error: return is not allowed here
}

即便内部使用没有什么嵌套关系, 如果函数的设计者想禁止non-local return, 也是可以直接将参数标记为crossinline的.

inline fun fooWithCrossinline(crossinline body: () -> Unit) {
    println("with crossinline start")
    body()
    println("with crossinline done")
}

使用的时候, 如果企图non-local return也是同样报错:

fooWithCrossinline {
    return // return is not allowed here
}

crossinline总结一下:

  • 我怎么知道某个inline函数的某个lambda参数在内部使用时到底有没有嵌套关系? -> 如果有嵌套, 它必定被标记为crossinline, 必定不能non-local return.
  • 虽然没有嵌套关系, 但是想禁止在lambda中直接return外部函数 -> 把参数标记为crossinline.

reified

有时候我们需要类型作为参数, 但是又觉得函数声明个clazz: Class<T>参数, 传入实参MyClass::class.java这样比较难看.

我这么说可能不太好明白, 还是举个例子吧.

比如这是一个查找某个类型实例的查找方法:

interface Hero
class SuperMan : Hero
class Hulk : Hero
class IronMan : Hero

fun <T> findHero1(candidates: List<Hero>, clazz: Class<T>): T? {
    candidates.forEach {
        if (clazz.isInstance(it)) {
            @Suppress("UNCHECKED_CAST")
            return it as T
        }
    }
    return null
}

调用这个方法的时候, 参数是这么传的:

findHero1(candidates, Hulk::class.java)

能不能就只传入类名呢?

既然这么问了当然是可以的. inline函数支持reified type parameters, 可以写成这样:

inline fun <reified T> findHero2(candidates: List<Hero>): T? {
    candidates.forEach {
        if (it is T) {
            return it as T
        }
    }
    return null
}

此时调用查找方法:

findHero2<SuperMan>(candidates)

用了reified之后, T可以直接当做类型来使用了, 并且不再需要反射, isas等操作符都可以用了. 也去掉了那个丑陋的@Suppress.

注意:

  • 只有inline函数的参数可以被标记为reified.
  • 只有runtime-available的类型可以被传入reified类型的参数. Nothing, List不行.

访问限制

因为函数默认是public的, 当一个方法inline之后, 它就作为public API了, 不能访问私有字段.

把字段改为internal并且加上注解@PublishedApi之后可以访问:

class PublishedApiDemo {
    @PublishedApi
    internal var internalField = "internal published api"
    private var somePrivateField = "private field"

    inline fun someInlineFun(body: () -> Unit) {
        //somePrivateField.length //ERROR
        body()
        internalField //OK
    }
}

Recap

  • inline解决函数调用开销.
  • noinline阻止参数被inline.
  • crossinline阻止non-local return.
  • reified让类型参数更加具体, 好用.

参考