[Kotlin Tutorials 17] Kotlin中的inline, noinline, crossinline, reified

Kotlin中的inline, noinline, crossinline, reified

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

本篇文章介绍Kotlin的inline函数, 顺一顺相关的知识点, 解决这些问题.
本文收录于: https://github.com/mengdd/KotlinTutorials

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<T>不行.

访问限制

因为函数默认是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让类型参数更加具体, 好用.

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容