Kotlin的inline noinline crossinline笔记

简介

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

inline

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

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

比如如下代码:

// Kotlin
fun main(args: Array) {
    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) {
    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) {
        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) {
    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) {
        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) {
        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 crossinline笔记)