美文网首页kotlin
Kotlin inline, noinline and cros

Kotlin inline, noinline and cros

作者: 弄码哥nomag | 来源:发表于2020-02-09 10:31 被阅读0次

    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

    相关文章

      网友评论

        本文标题:Kotlin inline, noinline and cros

        本文链接:https://www.haomeiwen.com/subject/ayjgxhtx.html