Kotlin syntax advanced - scope functions and application scenarios

The Kotlin standard library provides several functions: let, run, with, apply and also, whose sole purpose is to execute a block of code in the context of an object. When such a function is called on an object and a lambda expression is provided, it forms a temporary scope in which the object can be accessed without its name. These functions are called scope functions.
What these functions
have in common is that they execute a block of code on an object.
The difference: how this object is used in the code block, and what the entire expression returns.

run and apply functions

run function: the context object is the receiver of the lambda expression, executes a lambda expression and returns the result of the lambda expression

public inline fun <T, R> T.run(block: T.() -> R): R {
    
    
    contract {
    
    
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

apply function: the context object is the receiver of the lambda expression, executes a lambda expression and returns the context object

public inline fun <T> T.apply(block: T.() -> Unit): T {
    
    
    contract {
    
    
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

What the run and apply functions have in common is that the context object serves as the receiver of the lambda expression, but the return results are different. The run function returns the result of the lambda expression, and the apply function returns the context object itself.
Usage scenarios:
Both the run and apply functions can be understood as assigning a value to an object or performing certain operations, and both can and are often used for object initialization.
The return value of the run function is the result of the lambda expression, so it is often used in scenarios where a return value is required after performing some (initialization) operations.
The return value of the apply function is the context object itself, so it can be used in chain calls to perform some operations on objects in a certain link in the call chain.

Example 1: The following is an initialization operation. There are no requirements for the return value. We can use run or apply function. There is no difference.

binding.recyclerView.run {
    
    
  layoutManager = LinearLayoutManager(this)
  addOnItemTouchListener(RecyclerView.SimpleOnItemTouchListener())
}
或者
binding.recyclerView.apply{
    
    
  layoutManager = LinearLayoutManager(this)
  addOnItemTouchListener(RecyclerView.SimpleOnItemTouchListener())
}

Example 2: As follows, we need to perform some operations on a Person, increase the age by 10 years, and finally return the increased age by 10 years to a constant. We cannot use apply here, only run, because we do not need the Person. itself, but the increased age, which is the result of the lambda expression

data class Person(var name: String, var sex: String, var age: Int) {
    
    
  override fun toString(): String {
    
    
    return "姓名:$name , 性别:$sex , 年龄:$age"
  }
}

val user = Person("小慧", "女", 18)
val ageOlder = user.run {
    
    
  age += 10
  Log.e("时光流逝", "$name 的年龄增加了10岁")
  age
}

Example 3: As follows, we make chain calls, modify the Person object midway, and finally print the modified Person object. At this time, we need to use apply instead of run, because what we need is the Person object itself, not the lambda expression. The result of the formula

data class Person(var name: String, var sex: String, var age: Int) {
    
    
  override fun toString(): String {
    
    
    return "姓名:$name , 性别:$sex , 年龄:$age"
  }
}
fun String.logE(tag: String) {
    
    
  Log.e(tag, this)
}

val user = Person("小慧", "女", 18)
user.apply {
    
    
name = "小米"
sex = "男"
age = 20
}.toString().logE("人物信息")

with function

with function: specifies the receiver of the lambda expression, executes a lambda expression and returns the result of the lambda expression, very similar to the run function.

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    
    
    contract {
    
    
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

Usage scenario: Obtain a result after performing certain operations on an object. The semantics are stronger. The usage scenario is to introduce an auxiliary object whose properties or functions will be used to calculate a value.
Case: The above code can be modified into the with function, using user to calculate ageOlder.

val ageOlder = with(user) {
    
    
  age += 10
  Log.e("时光流逝", "$name 的年龄增加了10岁")
  age
}

let function and also function

let function: the context object is the parameter of the lambda expression, executes a lambda expression and returns the result of the lambda expression

public inline fun <T, R> T.let(block: (T) -> R): R {
    
    
    contract {
    
    
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

also function: the context object is the parameter of the lambda expression, executes a lambda expression and returns the context object itself

public inline fun <T> T.also(block: (T) -> Unit): T {
    
    
    contract {
    
    
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

What the let and also functions have in common is that the context object is used as the parameter it of the lambda expression, but the return results are different. The let function returns the result of the lambda expression, and the also function returns the context object itself.

Usage scenarios of let function:
1. Call one or more functions on the result of the call chain:

official example:

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map {
    
     it.length }.filter {
    
     it > 3 }
println(resultList)
替换为:
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map {
    
     it.length }.filter {
    
     it > 3 }.let {
    
     
    println(it)
    // 如果需要可以调用更多函数
} 

2.let is often used to execute a block of code using only non-null values. To perform operations on a non-null object, use the safe call operator?. on it and call let to perform the operation in a lambda expression.

Official example:

val str: String? = "Hello" 
//processNonNullString(str)       // 编译错误:str 可能为空
val length = str?.let {
    
     
    println("let() called on $it")
    processNonNullString(it)      // 编译通过:'it' 在 '?.let { }' 中必不为空
    it.length
}

3. Another situation where let is used is to introduce local variables with limited scope to improve the readability of the code. To define a new variable for the context object, provide its name as a lambda expression parameter instead of the default it.

Official example:

val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let {
    
     firstItem ->
    println("The first item of the list is '$firstItem'")
    if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.toUpperCase()
println("First item after modifications: '$modifiedFirstItem'")

Usage scenarios of also:
also is useful for performing some operations that take a context object as a parameter. Use also when you need to refer to an object rather than its properties and functions, or when you don't want to block this references from the outer scope.

Official example:

val numbers = mutableListOf("one", "two", "three")
numbers
    .also {
    
     println("The list elements before adding new one: $it") }
    .add("four")

Summary: The usage of let, run, with, apply and also are very similar. In many cases, they can be used interchangeably. The specific usage specifications can be based on personal understanding and the company's development specifications.
For example:
run and apply are generally used for initialization operations (more inclined to assignment operations). Because apply returns the context object itself, apply is also often used in chain calls, in which an object in the chain is assigned and modified. Wait for operations.
with is generally used to assign the result of calculation and processing based on an object to another object. It is very similar to let. There are
three usages in official documents: 1.) Call one or more functions on the result of the call chain 2.) Cooperate Safe call operator?. Perform non-null operations 3.) Introduce local variables with limited scope to improve code readability.
Also used for operations that require referencing objects instead of their properties and functions. It is very similar to apply and difficult Distinction: The context object of apply is the receiver of the lambda expression, and the context object of also is the parameter of the lambda expression; and apply is mostly used for assignment calculation of variables of the context object, while also is used for the operation of the context object itself. In fact, the boundary between the two can be completely determined by one's own understanding or norms.

takeIf and takeUnless functions

takeif function:

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    
    
    contract {
    
    
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}

takeUnless function

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    
    
    contract {
    
    
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (!predicate(this)) this else null
}

takeIf function: The context object is the parameter of the lambda expression. The lambda expression returns a Boolean value. If it is true, it returns the context object itself, otherwise it returns null. That is to say, a piece of logic is executed on the context object. If the logic is true, the context object is returned. If the logic is false, null is returned. The takeUnless function has the opposite logic to the takeIf function. No need to think about application scenarios: data is filtered according to conditions.

Official example:

val number = Random.nextInt(100)

val evenOrNull = number.takeIf {
    
     it % 2 == 0 }
val oddOrNull = number.takeUnless {
    
     it % 2 == 0 }

When chaining calls to other functions after takeIf and takeUnless, don't forget to perform null checks or safe calls (?.), because their return values ​​are nullable.

Guess you like

Origin blog.csdn.net/weixin_43864176/article/details/128483725