Kotlin inline, noinline and crossinline

Kotlin inline, noinline and crossinline

tags: Kotlin inline, noinline, crossinline

简介

kotlin 中,有三个类似的概念,inlinenoinlinecrossinline。平时使用的时候,很容易混淆。本文会介绍这三个概念的用法以及区别。

inline

inline 就是我们常说的内联。这个关键字会在编译期间起作用。如果一个函数是 inline 的,那么编译器会在编译的时候,把这个函数复制到调用处。这样做有什么好处呢?总的来说,好处有三个:

第一点,会减少函数调用的次数。我们知道,虽然函数调用的开销很小,但是确实是有一定的开销的。尤其是在大量的循环中,这种开销会变得更加明显。

比如如下代码:

// Kotlin
fun main(args: Array<String>) {
    multiplyByTwo(5)
}
fun multiplyByTwo(num: Int) : Int {
    return num * 2
}

他进行反编译之后的等价 Java 代码如下:

    // Java
public static final void main(@NotNull String[] args) {
   //...
   multiplyByTwo(5);
}

public static final int multiplyByTwo(int num) {
   return num * 2;
}

可以看到,不加 inline 的方法,编译成字节码,然后再反编译成等价 java 代码,得到的结果是一个普通的方法。这个跟我们的常识是吻合的。

但是,当我们把方法用 inline 修饰了之后,会发生什么呢?

比如如下代码中,我们把 multiplyByTwoinline 参数修饰了一下:

// Kotlin
fun main(args: Array<String>) {
    multiplyByTwo(5)
}
inline fun multiplyByTwo(num: Int) : Int {
    return num * 2
}

反编译得到的结果如下:

// Java
public static final void main(@NotNull String[] args) {
   // ...
   int num$iv = 5;
   int var10000 = num$iv * 2;
}

public static final int multiplyByTwo(int num) {
   return num * 2;
}

可以看到,inline 中的方法,被复制到了调用方。这就是 inline 威力强大的地方!

第二点,会减少对象的生成。当方法中,有一个参数是 lambda 的时候,使用 inline 的方法,可以减少对象的生成。kotlin 对于默认的 lambda 参数的处理方式为,把 lambda 转化成一个类,看起来跟 java 中的匿名内部类非常相似。

比如,

  // Kotlin
    fun main(args: Array<String>) {
        val methodName = "main"
        multiplyByTwo(5) { result: Int -> println("call method $methodName, Result is: $result") }
    }

    fun multiplyByTwo(num: Int,
                      lambda: (result: Int) -> Unit): Int {
        val result = num * 2
        lambda.invoke(result)
        return result
    }

反编译之后的结果有点复杂:

 public final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      final String methodName = "main";
      this.multiplyByTwo(5, (Function1)(new Function1() {
         public Object invoke(Object var1) {
            this.invoke(((Number)var1).intValue());
            return Unit.INSTANCE;
         }

         public final void invoke(int result) {
            String var2 = "call method " + methodName + ", Result is: " + result;
            boolean var3 = false;
            System.out.println(var2);
         }
      }));
   }

   public final int multiplyByTwo(int num, @NotNull Function1 lambda) {
      Intrinsics.checkParameterIsNotNull(lambda, "lambda");
      int result = num * 2;
      lambda.invoke(result);
      return result;
   }

观察生成的结果:java 生成了一个 Function1 类型的对象,来表示这个 lambda。其中,Funtion1 中的 1 就代表这个 lambda 值需要一个参数。类似的,如果是不需要参数的,那么就是 Function0。这个生成的结果,跟我们平时写 java 代码的时候使用的匿名内部类的方式是一样的。那么,可想而知,如果这个 lambda 是在一个循环中被调用的,那么就会生成大量的对象。

既然,inline 有如上的好处,那么是否有什么“坏处”,或者会造成我们使用不方便的地方呢?

首先是,对于一个 publicinline 方法,他不可以引用类的私有变量。比如:

    private val happy = true
    
    inline fun testNonPrivateField() {
        println("happy = ${happy}")
    }

如果这么写代码,编译器会对 happy 报错。道理也很简单:既然 inline 是在编译期间复制到调用方,那么自然就不能引用类的私有变量,因为调用方很大可能应该是“看不见”这个私有变量的。

其次,inline 方法会对流程造成非常隐晦的影响。

// Kotlin
fun main(args: Array<String>) {
    println("Start of main")

    multiplyByTwo(5) {
        println("Result is: $it")
        return
    }

    println("End of main")
}
// Java
public static final void main(@NotNull String[] args) {
   String var1 = "Start of main";
   System.out.println(var1);
   int num$iv = 5;
   int result$iv = num$iv * 2;
   String var4 = "Result is: " + result$iv;
   System.out.println(var4);
}

观察上面的两端代码,我们发现在反编译出来的 java 代码中,没有找到 “End of main”。为什么呢?原因其实很简单:根据我们前面知道的,inline 其实就是把代码在编译期间复制到调用方,因此,如果 lambda 中有 return 语句,那么也会被原样复制过去,进而,因为 lambda 中的 return 的影响,导致编译器认为后面的 “End of main” 其实是不能被访问到的代码,于是在编译期间给去掉了。

所以,小结一下:inline 关键字的作用,是把 inline 方法以及方法中的 lambda 参数在编译期间复制到调用方,进而减少函数调用以及对象生成。

不过,inline 关键字对于 lambda 的处理有的时候不是我们想要的。也就是,有时我们不想让 lambda 也被 inline。那么有什么办法呢?这个时候就需要 noinline 关键字了。

noinline

noinline 修饰的是 inline 方法中的 lambda 参数。noinline 用于我们不想让 inline 特性作用到 inline 方法的某些 lambda 参数上的场景。

比如:

    // Kotlin
    fun main(args: Array<String>) {
        val methodName = "main"
        multiplyByTwo(5) {
            result: Int -> println("call method $methodName, Result is: $result")
        }
    }

    inline fun multiplyByTwo(
            num: Int,
            noinline lambda: (result: Int) -> Unit): Int {
        val result = num * 2
        lambda.invoke(result)
        return result
    }

反编译的结果是:

 public final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      final String methodName = "main";
      byte num$iv = 5;
      Function1 lambda$iv = (Function1)(new Function1() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1) {
            this.invoke(((Number)var1).intValue());
            return Unit.INSTANCE;
         }

         public final void invoke(int result) {
            String var2 = "call method " + methodName + ", Result is: " + result;
            boolean var3 = false;
            System.out.println(var2);
         }
      });
      int $i$f$multiplyByTwo = false;
      int result$iv = num$iv * 2;
      lambda$iv.invoke(result$iv);
   }

   public final int multiplyByTwo(int num, @NotNull Function1 lambda) {
      int $i$f$multiplyByTwo = 0;
      Intrinsics.checkParameterIsNotNull(lambda, "lambda");
      int result = num * 2;
      lambda.invoke(result);
      return result;
   }

可以看到, 因为使用了 noinline 修饰了 lambda,所以,编译器使用了匿名内部类的方式来处理这个 lambda,生成了一个 Function1 对象。

crossinline

是不是有了 inlinenoinline,对于我们开发人员来讲就够了呢?就满足了呢?显然不是的。考虑一种情况,我们既想让 lambda 也被 inline,但是又不想让 lambda 对调用方的控制流程产生影响。这个产生影响,可以是有意识的主动控制,但是大多数情况下是开发人员的不小心导致的。我们知道 java 语言是一个编译型语言,如果能在编译期间对这种 inline lambda 对调用方产生控制流程影响的地方进行提示甚至报错,就万无一失了。

crossinline 就是为了处理这种情况而产生的。crossinline 保留了 inline 特性,但是如果想在传入的 lambda 里面 return 的话,就会报错。return 只能 return 当前的这个 lambda

    // Kotlin
    fun main(args: Array<String>) {
        val methodName = "main"
        multiplyByTwo(5) {
            result: Int -> println("call method $methodName, Result is: $result")
            return@multiplyByTwo
        }
    }

如面代码所示,必须 return@multiplyByTwo,而不能直接写 return

总结

inline 关键字的作用,是把 inline 方法以及方法中的 lambda 参数在编译期间复制到调用方,进而减少函数调用以及对象生成。对于有时候我们不想让 inline 关键字对 lambda 参数产生影响,可以使用 noline 关键字。如果想 lambda 也被 inline,但是不影响调用方的控制流程,那么就要是用 crossinline

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

推荐阅读更多精彩内容