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 errorSince 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