SoFunction
Updated on 2025-04-10

Detailed introduction to the application of Kotlin scoped functions

I usually read blogs or learn knowledge, and the things I learned are relatively scattered, there is no independent concept of knowledge modules, and it is easy to forget after learning. So I built my ownNote warehouse(A note warehouse that I have maintained for a long time. If you are interested, you can click on a star~ Your star is a huge motivation for my writing). Classify everything I learned and put it in it. It is also convenient for review when needed.

1. Pre-knowledge

In Kotlin, a function is a first-class citizen and it also has its own type. for example()->Unit, function types can be stored in variables.

The function type in Kotlin is like:()->Unit(Int,Int)->StringInt.(String)->Stringwait. They have parameters and return values.

The last oneInt.(String)->StringStrangely, it means that the function type can have an additional receiver type. This means that a String type parameter can be called on the Int object and a String type function can be returned.

val test: Int.(String) -> String = { param ->
    "$this param=$param"
}
println(("2"))
println(test(1, "2"))

If we putInt.(String) -> StringThe type is defined as a variable and assigns a value to it. The parameter param of the subsequent Lambda is the incoming String type, and the final return value is also String. In this Lambda, this is used to represent the object of the previous receiver type Int, which is a bit like an extension function. You can access some member variables, member methods, etc. through this within the function. This type of function with the receiver can be regarded as a member method.

Because it is declared a bit like an extension function, we can use("2")To call the function type test, it will actually pass the Int 1 as a parameter after compilation. So the lattertest(1, "2")This method of calling is OK.

With the above knowledge supplement, let's look at Kotlin's standard library function apply

public inline fun <T> (block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
  • First of all, apply is an extension function, and secondly, it is generic, meaning that any object can call the apply function.
  • Then its parameters are the function type with the receiver, and the receiver is T. Then calling block() is like calling a member function in the T object. This can be used inside the block function to access the public member variables and the public member functions.
  • Return value: T, which object calls the extension function to return which object

2. Use

The scope function is built-in in Kotlin, which can operate and convert data, etc.

Let's first look at a demo, let and run

data class User(val name: String)
fun main() {

    val user = User("Clouds and Sky")
    val letResult =  { param -&gt;
        "let Output something ${}"
    }
    println(letResult)
    val runResult =  {  //this:User
        "run Output something ${}"
    }
    println(runResult)
}

Let and run are similar, and both return the execution result of Lambda. The difference is that let has Lambda parameters, while run does not. But run can use this to access public properties and functions in the user object.

Also and apply are similar

 { param->
    println("also ${}")
}.apply { //this:User
    println("apply ${}")
}

Also and apply return the currently executed object, also has Lambda parameters (the Lambda parameters here are the currently executed object), while apply does not have Lambda parameters (but accesses the currently executed object through this).

repeat is to repeat the execution of the current Lambda

repeat(5) {
    println()
}

with is more special. It does not exist in the form of an extension method, but a top-level function.

with(user) { //this: User
    println("with ${}")
}

With Lambda has no parameters inside, but can access public properties and functions of the incoming object through this.

3. Source code appreciation

If you use this, I won’t say much. I believe everyone is already very familiar with it, so we will start to appreciate the source code directly.

3.1 let and run

//Let and run are similar, and both return the execution result of Lambda. The difference is that let has Lambda parameters, while run does not.  But run can use this to access public properties and functions in the user object.public inline fun &lt;T, R&gt; (block: (T) -&gt; R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
public inline fun &lt;T, R&gt; (block: T.() -&gt; R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
  • Let and run are both extension functions
  • The Lambda of let has parameters, which is T, which is the object to be expanded, so the parameter can be accessed within the Lambda to access the internal public properties and functions of the parameter object.
  • The Lambda of run has no parameters, but this Lambda is the extension of the object T to be expanded. This is the function type with the receiver. So it can be regarded as the Lambda is a member function of T. Directly calling the Lambda is equivalent to directly calling the member function of the T object. Therefore, within this Lambda, you can access T's public properties and functions through this (only public ones can be accessed, and why is explained later).
  • Let and run are both the execution results of the returned Lambda

3.2 also and apply

//Also and apply both return to the original object itself, the difference is that apply does not have Lambda parameters, but also haspublic inline fun &lt;T&gt; (block: (T) -&gt; Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
public inline fun &lt;T&gt; (block: T.() -&gt; Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
  • Also and apply are extension functions
  • Also and apply both return to the original object itself, the difference is that apply does not have Lambda parameters, but also has
  • The Lambda of also has parameters, and the parameter is T, that is, the object to be expanded, so the parameter can be accessed within the Lambda, thereby accessing the internal public properties and functions of the parameter object.
  • The apply Lambda has no parameters, but this Lambda is the extension of the object T to be expanded. This is the function type with the receiver. So it can be regarded as the Lambda is a member function of T. Directly calling the Lambda is equivalent to directly calling the member function of the T object. Therefore, within this Lambda, you can access T's public properties and functions through this (only public ones can be accessed, and why is explained later).

3.3 repeat

public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }
    for (index in 0 until times) {
        action(index)
    }
}
  • repeat is a top-level function
  • This function has 2 parameters, one is the number of repetitions, and the other is the Lambda to be executed. Lambda has parameters, which indicates how many times it is executed
  • The function is very simple inside, it is just a for loop that executes Lambda

3.4 with

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return ()
}
  • with is a top-level function
  • with has 2 parameters, one is the receiver and the other is a function with the receiver
  • The return value of with is the return value of the block function
  • block is an extension of T, so you can use the receiver object to directly call the block function, and this can be used inside the block to access T's public properties and functions

4. Decompile

Learn what these scope functions look like after compilation, first look at the demo

data class User(val name: String)
fun main() {
    val user = User("Clouds and Sky")
    val letResult =  { param -&gt;
        "let Output something ${}"
    }
    println(letResult)
    val runResult =  {  //this:User
        "run Output something ${}"
    }
    println(runResult)
     { param -&gt;
        println("also ${}")
    }.apply { //this:User
        println("apply ${}")
    }
    repeat(5) {
        println()
    }
    val withResult = with(user) { //this: User
        println("with ${}")
        "with Output something ${}"
    }
    println(withResult)
}

Then decompile and look at the decompilation of data class. We will not read the decompilation of data class, we only focus on the internal code of main

User user = new User("Clouds and Sky");
("let output something" + ());
("Run output something" + ());
User $this$test_u24lambda_u2d3 = user;
("also " + $this$test_u24lambda_u2d3.getName());
("apply " + $this$test_u24lambda_u2d3.getName());
for (int i = 0; i &lt; 5; i++) {
    int i2 = i;
    (());
}
User $this$test_u24lambda_u2d5 = user;
("with " + $this$test_u24lambda_u2d5.getName());
("with output something" + $this$test_u24lambda_u2d5.getName());

You can see that all the things executed by let, run, also, apply, repeat, and with Lambda are put outside (because of inline), and there is no need to convert Lambda into Function (anonymous internal classes or something), so the execution performance will be much higher.

Well...I actually want to see itblock: T.() -> RWhat does this kind of compiled look like? The scope functions above are all inline functions, so I can't see it. I will write one by myself and write a few functions like let, run, and with, but without inline:

public fun &lt;T, R&gt; (block: (T) -&gt; R): R {
    return block(this)
}
public fun &lt;T, R&gt; (block: T.() -&gt; R): R {
    return block()
}
public fun &lt;T, R&gt; withMy(receiver: T, block: T.() -&gt; R): R {
    return ()
}
fun test() {
    val user = User("Clouds and Sky")
    val letResult =  { param -&gt;
        "let Output something ${}"
    }
    println(letResult)
    val runResult =  {  //this:User
        "run Output something ${}"
    }
    println(runResult)
    val withResult = withMy(user) { //this: User
        println("with ${}")
        "with Output something ${}"
    }
    println(withResult)
}

Decompiled looks:

final class TestKt$test$letResult$1 extends Lambda implements Function1&lt;User, String&gt; {
    public static final TestKt$test$letResult$1 INSTANCE = new TestKt$test$letResult$1();
    TestKt$test$letResult$1() {
        super(1);
    }
    public final String invoke(User param) {
        (param, "param");
        return "let output something" + ();
    }
}
final class TestKt$test$runResult$1 extends Lambda implements Function1&lt;User, String&gt; {
    public static final TestKt$test$runResult$1 INSTANCE = new TestKt$test$runResult$1();
    TestKt$test$runResult$1() {
        super(1);
    }
    public final String invoke(User $this$runMy) {
        ($this$runMy, "$this$runMy");
        return "Run output something" + $this$();
    }
}
final class TestKt$test$withResult$1 extends Lambda implements Function1&lt;User, String&gt; {
    public static final TestKt$test$withResult$1 INSTANCE = new TestKt$test$withResult$1();
    TestKt$test$withResult$1() {
        super(1);
    }
    public final String invoke(User $this$withMy) {
        ($this$withMy, "$this$withMy");
        ("with " + $this$());
        return "with output something" + $this$();
    }
}
public final class TestKt {
    public static final &lt;T, R&gt; R letMy(T $this$letMy, Function1&lt;? super T, ? extends R&gt; block) {
        (block, "block");
        return ($this$letMy);
    }
    public static final &lt;T, R&gt; R runMy(T $this$runMy, Function1&lt;? super T, ? extends R&gt; block) {
        (block, "block");
        return ($this$runMy);
    }
    public static final &lt;T, R&gt; R withMy(T receiver, Function1&lt;? super T, ? extends R&gt; block) {
        (block, "block");
        return (receiver);
    }
    public static final void test() {
        User user = new User("Clouds and Sky");
        ((String) letMy(user, TestKt$test$letResult$));
        ((String) runMy(user, TestKt$test$runResult$));
        ((String) withMy(user, TestKt$test$withResult$));
    }
}

In the demo I wrote, letMy, runMy, and Lambdas of withMy are all compiled into anonymous inner classes, and they all inherit fromThis class has been implementedFunction1<User, String>interface.

abstract class Lambda<out R>(override val arity: Int) : FunctionBase<R>, Serializable {
    override fun toString(): String = (this)
}
interface FunctionBase<out R> : Function<R> {
    val arity: Int
}
public interface Function<out R>
public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}

Lambda here is a built-in class in Kotlin, which is a Function used to represent the value of the function type. Function1 is inherited from Function, which represents a function type with a parameter. In addition to Function1, Kotlin also has built-in Function2, Function3, Function4, etc., representing the function types of 2, 3, and 4 parameters respectively. It's that simple and crude.

Going back to the above decompiled code, we find the letMy function, pass in the user object andTestKt$test$letResult$When executing this singleton object, the invoke function is called with the singleton object and then the user is passed in. existTestKt$test$letResult$1#invoke, the user object is received, and its function is accessed through the object. You can see that here you use the user object to access properties or functions in the object, so you must only access public properties and functions, which answers the above questions.

The other two, runMy and withMy functions, actually look exactly the same as letMy after compilation. This meansblock: (T) -> Randblock: T.() -> RIt's similar, the code is exactly the same after compilation. All of them pass the T object into the invoke function, and then operate the T object inside the invoke function.

5. Summary

Kotlin scope function is used very frequently in daily coding, so we need to briefly understand its basic principles and find problems if something happens. To understand scoped functions, you must first understand the function type. In Kotlin functions also have types, such as:()->Unit(Int,Int)->StringInt.(String)->Stringetc. They can be stored with variables. let, run, apply, and also are all extension functions, with and repeat are top-level functions, they are all functions modified by inline. After compilation, Lambda is gone, and the code inside Lambda is directly moved outside, improving performance.

Thank you for watching, I hope this article can help you understand scope functions more deeply.

After-class exercises:

If you feel that you have fully understood this article, you might as well take out the text editor and write down let, run, apply, also, with, repeat, which may have a deeper understanding effect.

This is the end of this article about the detailed introduction of Kotlin scope function application. For more related Kotlin scope content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!