Introduction to the basics of Kotlin language: Lambda expression

Posted by Tjorriemorrie on Fri, 18 Feb 2022 21:28:01 +0100

What is a Lambda expression?

Lambda expressions are actually anonymous functions. In fact, a function is a function. An anonymous function is an anonymous function code. In Kotlin, function also appears as a type. Although in the current version, the flexibility of function type is not as flexible as Python, it can also be assigned and passed, which is mainly reflected in lambda expressions.

Let's take a look at an example of Lambda expression:

 fun main(args: Array<String>) { 
     val lambda = { 
         left: Int, right: Int 
         -> 
         left + right 
     } 
     println(lambda(2, 3)) 
 } 

As you can see, we define a variable Lambda and assign it to a Lambda expression. The Lambda expression is enclosed by a pair of braces. After that, write down the parameters and their types in turn. If not, don't write it, and then write - >. This indicates that the following is the function body. The expression result of the last sentence of the function body is the return value of the Lambda expression. For example, the return value here is the result of parameter summation.

Later, we call this Lambda expression in the form of (). In fact, this () corresponds to the invoke method. In other words, we can also write here:

 println(lambda.invoke(2,3)) 

The two calls are written in exactly the same way.

There is no doubt that the output of this code should be 5.

Simplified Lambda expression

Let's take another example:

 fun main(args: Array<String>) { 
     args.forEach { 
        if(it == "q") return 
        println(it) 
     } 
     println("The End") 
 } 

args is an array. We have seen the example of iterative array of for loop, but we actually have more modern means to iterate an array, such as the above example. It's nothing to be afraid of. Once you tear off its mask, you'll find that you already know it:

 public inline fun <T> forEach(action: (T) -> Unit): Unit { 
     for (element in this) action(element) 
 } 

This is an extension method. The extension method is easy to understand. The original class does not have this method. We extend it with a new method externally. This new method is the extension method. We all regard it as the method defined by Array. We can see that it is actually a for loop, right? What does the for loop do? The Lambda expression we passed in is called, and each element is passed in as a parameter. So what should we write when we call the forEach method?

 args.forEach({ 
     element -> println(element) 
 }) 

What is this equivalent to?

 for(element in args){ 
    println(element) 
 } 

Is it easy to understand?

Next, Kotlin allows us to remove the last Lambda expression parameter of the function from the parentheses, that is, we can change the writing of forEach above:

 args.forEach(){ 
     element -> println(element) 
 } 

It looks a bit like a function definition, but the difference is still obvious. Don't be dizzy at this time. If you are dizzy, I have some carsickness medicine here.

It's not over yet. If the function has only one Lambda expression parameter, it's just the one in front. It's useless to leave a parenthesis. Just throw it away:

 args.forEach{ 
     element -> println(element) 
 } 

How are you? You think it's over? nonono, if the Lambda expression passed in has only one parameter, or for example, the forEach parameter above has only one element, we can also omit it when calling, and call it it it by default. It makes sense. It's not it, although it actually means iterator:

 args.forEach{ 
      println(it) 
 } 

Well, almost. Is it over? No. Are you finished? This is the only one left. If there is only one function call in this Lambda expression and the parameter of this function is also the parameter of this Lambda expression, you can also simplify the above code by function reference:

 args.forEach(::println) 

Is this a bit like the function pointer in C? Functions are also objects. It's no surprise. As long as the input and return values of actual parameters such as println are consistent with the requirements of formal parameters, it can be so simplified.

To sum up:

  1. The last Lambda can be removed
  2. There is only one Lambda, and parentheses can be omitted
  3. Lambda has only one parameter, which can be defaulted to it
  4. Functions whose input parameters and return values are consistent with formal parameters can be passed in as arguments by function reference

In this way, we can roughly understand the example we gave earlier:

 fun main(args: Array<String>) { 
     args.forEach { 
        if(it == "q") return 
        println(it) 
     } 
     println("The End") 
 }

Return from Lambda

Do you really understand? Suppose the parameter I enter is

 o p q r s t 

Do you know what to output?

 o 
 p 
 The End 

Am I right?

No, return will directly end the main function. Why? Lambda expression is an expression. Although it looks like a function and functions like a function, it looks like a code block.

So, there's no way to return? Of course not. Soldiers will block the water and cover the earth:

 fun main(args: Array<String>) { 
     args.forEach forEachBlock@{ 
        if(it == "q") return@forEachBlock 
        println(it) 
     } 
     println("The End") 
 } 

Just define a label. You can also return@forEachBlock Add your return value if necessary.

Type of Lambda expression

OK, the Lambda expression mentioned earlier is actually a function type. The Lambda expression we passed in the forEach method is actually a parameter of the forEach method. Let's look at the definition of forEach:

 public inline fun <T> Array<out T>.forEach(action: (T) -> Unit): Unit { 
     for (element in this) action(element) 
 } 

Note that the type of action parameter is (T) - > Unit, which is the type of Lambda expression or function. It means that the function accepts a parameter of type T and returns a result of type Unit. Let's take a few more examples:

 () -> Int //No parameter, Int returned  
 (Int, Int) -> String //Two integer parameters, return string type 
 (()->Unit, Int) -> Unit //A Lambda expression and an integer are passed in, and Unit is returned 

We usually use this form to represent the type of Lambda expression. Some people may say that since people are all types, why don't they have a name? Or, which class does it correspond to?

 public interface Function<out R> 

In this case, what if you implement all the expressions of Lambda? Where was it defined? Don't be funny to say it. Kotlin's developers have defined 23 sub interfaces of functions, of which Function n means that the invoke method has n parameters..

 public interface Function0<out R> : Function<R> { 
     public operator fun invoke(): R 
 } 
 public interface Function1<in P1, out R> : Function<R> { 
     public operator fun invoke(p1: P1): R 
 } 
 ... 
 public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> { 
     public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R 
 } 

To be honest, when I first saw this, I laughed directly. Kotlin developers are really black humor.

This can't be over. If I really have a function with more than 22 parameters, won't Kotlin support it?

 fun hello2(action: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) -> Unit) { 
     action(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22) 
 } 

Therefore, we define a Lambda expression with 23 parameters, and it is also rough to call the method:

 hello2 { i0, i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19, i20, i21, i22 -> 
     println("$i0, $i1, $i2, $i3, $i4, $i5, $i6, $i7, $i8, $i9, $i10, $i11, $i12, $i13, $i14, $i15, $i16, $i17, $i18, $i19, $i20, $i21, $i22,") 
 } 

Compile and run results:

 Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/Function23 
    at java.lang.Class.getDeclaredMethods0(Native Method) 
    at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) 
    at java.lang.Class.privateGetMethodRecursive(Class.java:3048) 
    at java.lang.Class.getMethod0(Class.java:3018) 

Sure enough, although 23 Lambda expressions with this parameter are mapped to the kotlin/Function23 class, this class does not exist, that is, for Lambda expressions with more than 22 parameters, Kotlin code can be compiled, but runtime exceptions will be thrown. Of course, this is not a thing. After all, who is so mentally disabled that more than 22 parameters are needed?

SAM conversion

The name is very tall. You may have seen a lot of simple things to fry chicken. You must not avoid such things. If you learn more, you can take out one more thing to scare people.

 val worker = Executors.newCachedThreadPool() 
   
 worker.execute { 
     println("Hello") 
 } 

We should have passed in an instance of Runnable, but the result was fooled by a Lambda expression

 GETSTATIC net/println/MainKt$main$1.INSTANCE : Lnet/println/MainKt$main$1; 
 CHECKCAST java/lang/Runnable 
 INVOKEINTERFACE java/util/concurrent/ExecutorService.execute (Ljava/lang/Runnable;)V 

Look at the three byte codes above. The first sentence gets an instance of a class, which is an anonymous inner class:

 final class net/println/MainKt$main$1 implements java/lang/Runnable  { 
    ... 
 } 

This is the bytecode part of this class definition, which implements a class of Runnable interface!

In the second sentence, after you get the instance of this class, do a strong conversion - what else to convert? Just use it directly. It must be no problem.

What are the conditions for SAM conversion?

  • First, the caller is in Kotlin, and the callee is Java code. If the worker in the previous example execute(...) If it is a method defined in Kotlin, we cannot convert it with SAM.
  • Secondly, the parameters must be Java interfaces, that is, Kotlin interfaces, abstract classes and Java abstract classes are not allowed.
  • Again, the Java interface of the parameter must have only one method.

Let's take another common example in Android:

 view.setOnClickListener{ 
    view -> 
    ... 
 } 

view.setOnClickListener(...) Is a Java method, the parameter OnClickListener is a java interface, and there is only one method:

 public interface OnClickListener { 
     void onClick(View v); 
 } 

kotlin data sharing

Advanced Kotlin enhanced combat

140 episode Kotlin introduction to proficient in a full series of video tutorials (project development and actual combat)

Topics: Android kotlin