An in depth study of local methods in Kotlin

  • 2021-10-16 02:39:02
  • OfStack

Preface

Kotlin is an open source programming language designed by JetBrains, a famous IDE manufacturer who has developed IntelliJ IDEA, Android Studio, PyCharm and other IDE. The Kotlin project launched in July 2011 was deeply influenced by "Effective Java", and it was not officially released until February 15, 2016, when the first official stable version Kotlin v1.0 was officially released. In the 2017 Google I/O Developer Conference, Google announced that Kotlin became the first-level language developed by Android, and Kotlin "turned positive"

In Kotlin, defining methods is interesting, not only because the keyword of the method is fun (the first few characters of function), but also because you will be surprised to find that it allows us to define methods in methods. As follows


fun methodA() {
 fun methodB() {

 }
 methodB() //valid
}

//methodB() invalid

Among them

methodB is defined in the method body of methodA, that is, methodB is called a local method or local function methodB method calls only in methodA methodB is called outside the methodA method, causing a compilation error

Since Kotlin supports local methods, what particular use should Kotlin have compared to it

First of all, its characteristics are as local as its name 1, which means that it has an incomparable ability to limit a smaller range. It ensures small-scale availability and isolates the possibility of potentially irrelevant calls.

As the golden rule in programming, the smaller the method, the better. Compared with the lengthy vertical code fragment, it will be divided into small local methods with function sheet 1 according to its responsibilities, and finally organized and called, which will make our code appear more organized and clear.

As a programmer, curiosity should be one of his qualities. We should want to study the implementation principle of local methods. At least we never saw this concept in Java era.

In fact, after careful study of this matter, there are still many details. Because local methods can capture external variables or not.

Here is one case of capturing external variables


fun outMethodCapture(args: Array<String>) {
 fun checkArgs() {
 if (args.isEmpty()) {
  println("innerMethod check args")
  Throwable().printStackTrace()
 }
 }
 checkArgs()
}

Among them, the local method checkArgs captures the parameter args of outMethodCapture.

Therefore, it is not difficult to understand that external variables are not captured, as follows, that checkArgs handles args through parameters.


fun outMethodNonCapture(args: Array<String>) {
 fun checkArgs(args: Array<String>) {
 if (args.isEmpty()) {
  println("outMethodNonCapture check args")
  Throwable().printStackTrace()
 }
 }
 checkArgs(args)
}

Firstly, we analyze the implementation principle of local method for capturing variables in 1


public static final void outMethodCapture(@NotNull final String[] args) {
 Intrinsics.checkParameterIsNotNull(args, "args");
 <undefinedtype> checkArgs$ = new Function0() {
 // $FF: synthetic method
 // $FF: bridge method
 public Object invoke() {
 this.invoke();
 return Unit.INSTANCE;
 }

 public final void invoke() {
 Object[] var1 = (Object[])args;
 if(var1.length == 0) {
  String var2 = "innerMethod check args";
  System.out.println(var2);
  (new Throwable()).printStackTrace();
 }

 }
 };
 checkArgs$.invoke();
}

As the above implementation principle, that is, the local method implementation is actually an instance of an anonymous internal class, and then it can be called again. For local methods that are not captured, it is slightly different. First, we decompile the corresponding Java code


public static final void outMethodNonCapture(@NotNull String[] args) {
 Intrinsics.checkParameterIsNotNull(args, "args");
 <undefinedtype> checkArgs$ = null.INSTANCE;
 checkArgs$.invoke(args);
}

What we got is an incomplete code, so we need to go to the project and analyze it with a corresponding class file. First, we find a file like this, MainKt $outMethodCapture $1. class (whose class file follows the rule of "file name $method name $internal class sequence number").

Use javap method to decompile and analyze the file again, note that the $symbol needs to be simply processed under 1.


➜ KotlinInnerFunction javap -c "MainKt\$outMethodNonCapture\$1.class"
Compiled from "Main.kt"
final class MainKt$outMethodNonCapture$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function1<java.lang.String[], kotlin.Unit> {
 public static final MainKt$outMethodNonCapture$1 INSTANCE;

 public java.lang.Object invoke(java.lang.Object);
 Code:
  0: aload_0
  1: aload_1
  2: checkcast  #11     // class "[Ljava/lang/String;"
  5: invokevirtual #14     // Method invoke:([Ljava/lang/String;)V
  8: getstatic  #20     // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
  11: areturn

 public final void invoke(java.lang.String[]);
 Code:
  0: aload_1
  1: ldc   #23     // String args
  3: invokestatic #29     // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
  6: aload_1
  7: checkcast  #31     // class "[Ljava/lang/Object;"
  10: astore_2
  11: aload_2
  12: arraylength
  13: ifne   20
  16: iconst_1
  17: goto   21
  20: iconst_0
  21: ifeq   44
  24: ldc   #33     // String outMethodNonCapture check args
  26: astore_2
  27: getstatic  #39     // Field java/lang/System.out:Ljava/io/PrintStream;
  30: aload_2
  31: invokevirtual #45     // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  34: new   #47     // class java/lang/Throwable
  37: dup
  38: invokespecial #51     // Method java/lang/Throwable."<init>":()V
  41: invokevirtual #54     // Method java/lang/Throwable.printStackTrace:()V
  44: return

 MainKt$outMethodNonCapture$1();
 Code:
  0: aload_0
  1: iconst_1
  2: invokespecial #61     // Method kotlin/jvm/internal/Lambda."<init>":(I)V
  5: return

 static {};
 Code:
  0: new   #2     // class MainKt$outMethodNonCapture$1
  3: dup
  4: invokespecial #80     // Method "<init>":()V
  7: putstatic  #82     // Field INSTANCE:LMainKt$outMethodNonCapture$1;
  10: return
}

The above class is actually relatively simple, and more importantly, it is the implementation of a singleton. Because this reduces the generation of anonymous inner classes and the creation of instances compared with the capture case, the theoretical cost will be smaller.

Considering the above comparison, it is more recommended to use a method that does not capture external variables when using local methods.

Attention to use

Yes, there is one consideration when using local methods, that is, a rule convention, that is, it needs to be defined before it can be used, otherwise an error will be reported, as shown below


fun outMethodInvalidCase(args: Array<String>) {
 checkArgs()//invalid unresolved reference
 fun checkArgs() {
  if (args.isEmpty()) {
   println("innerMethod check args")
   Throwable().printStackTrace()
  }
 }
 checkArgs()//valid
}

However, there are still some problems in defining local methods before using them, which are mainly manifested in the readability of the code.

Imagine 1. If you go into 1 method and see 1 series of local methods, it may be more or less awkward.

But imagine 1, since there is such a problem, why should it be designed like this? First of all, let's look at a small example


0fun outMethodInvalidCase(args: Array<String>) {
 checkArgs(args)
 var a = 0 //the reason why it's unresolved
 fun checkArgs(args: Array<String>) {
  if (args.isEmpty()) {
   println("outMethodNonCapture check args")
   Throwable().printStackTrace()
   a.toString()
  }
 }
}

Because local methods can be capture local variables, checkArgs captures the local variable a when line 1 of code checkArgs is called, and checkArgs appears to be defined, but line 2 has not been executed yet, causing compilation problems.

At present, capture variables and non-capture local method usage are all 1, which need to be defined before being used.

As for the local method in Kotlin, we can try to achieve the purpose of limiting the scope and splitting the method. When using it, we should try our best to choose the local method in the form of non-capture.

Summarize


Related articles: