Differences between Kotlin Scope Functions and Detailed Explanation of Use Scenarios

  • 2021-12-05 07:20:49
  • OfStack

Scope function

There are five scope functions of Kotlin: let, run, with, apply and also.

These functions basically do the same thing: execute one block of code on one object.

The following is a typical use of scope functions:


val adam = Person("Adam").apply { 
 age = 20
 city = "London"
}
println(adam)

If it is not implemented using apply, the name of a newly created object must be repeated each time it is assigned a value to its property.


val adam = Person("Adam")
adam.age = 20
adam.city = "London"
println(adam)

Scope functions don't introduce any new techniques, but they can make your code more concise and readable.

In fact, the same function can be achieved by multiple scope functions, but we should use appropriate scope functions according to different scenarios and requirements for more elegant implementation.

If you want to see the difference between scoped functions and the summary table of usage scenarios directly, please click here.

Next, we will describe the differences and conventional usage of these scope functions in detail.

Difference

There are two main differences between scope functions:

How Context Objects Are Referenced Return value

How to reference context objects: this or it

In the lambda expression of the scope function, the context object can be accessed using a shorter reference (this or it) instead of its actual name.
Scope functions refer to context objects in two ways:

As recipients of the lambda expression (this): run, with, apply As parameters of the lambda expression (it): let, also

fun main() {
 val str = "Hello"
 // this
 str.run {
 println("The receiver string length: $length")
 //println("The receiver string length: ${this.length}") //  Have the same effect as the previous sentence 
 }

 // it
 str.let {
 println("The receiver string's length is ${it.length}")
 }
}

As the recipient of an lambda expression

run, with, and apply refer to the context object as the recipient of the lambda expression through the keyword this. this can be omitted to make the code shorter.

Usage scenario: Mainly operate on members of context objects (access properties or call functions).


val adam = Person("Adam").apply { 
 age = 20  //  And  this.age = 20  Or  adam.age = 20 1 Sample 
 city = "London"
}
println(adam)

As a parameter of lambda expression

let and also take context objects as lambda expression parameters. If no parameter name is specified, the object can be accessed with the implicit default name it. it is shorter than this, and expressions with it are generally easier to read. However, an object cannot be accessed implicitly, as this does, when its functions or properties are called.

Usage scenario: Mainly operate on context objects and use them as parameters.


fun getRandomInt(): Int {
 return Random.nextInt(100).also {
 writeToLog("getRandomInt() generated value $it")
 }
}
val i = getRandomInt()

In addition, when the context object is passed as a parameter, you can specify a scoped custom name for the context object (to improve the readability of the code).


fun getRandomInt(): Int {
 return Random.nextInt(100).also { value ->
 writeToLog("getRandomInt() generated value $value")
 }
}
val i = getRandomInt()

Return value

According to the returned results, scoped functions can be divided into the following two categories:

Return context objects: apply, also Returns lambda expression results: let, run, with

You can choose the appropriate function according to the subsequent actions in the code.

Returns a context object

The return value of apply and also is the context object itself. Therefore, they can be included in the call chain as auxiliary steps: You can continue to make chain function calls on the same object.


val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
 .apply {
 add(2.71)
 add(3.14)
 add(1.0)
 }
 .also { println("Sorting the list") }
 .sort()

They can also be used in return statements of functions that return context objects.


fun getRandomInt(): Int {
 return Random.nextInt(100).also {
 writeToLog("getRandomInt() generated value $it")
 }
}

val i = getRandomInt()

Returns the result of an lambda expression

let, run, and with return the result of an lambda expression. Therefore, you can use them when you need to use their results to assign a value to a variable, or when you need to chain their results.


val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run { 
 add("four")
 add("five")
 count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")

In addition, you can ignore the return value and create a temporary scope for the variable using only the scope function.


val numbers = mutableListOf("one", "two", "three")
with(numbers) {
 val firstItem = first()
 val lastItem = last() 
 println("First item: $firstItem, last item: $lastItem")
}

Conventional usage

let

The context object is accessed as an argument to the lambda expression (it). The return value is the result of an lambda expression.

let can be used to call one or more functions on the result of the call chain. For example, the following code prints the results of two operations on the collection:


val adam = Person("Adam")
adam.age = 20
adam.city = "London"
println(adam)
0

Using let, you can write this:


val adam = Person("Adam")
adam.age = 20
adam.city = "London"
println(adam)
1

If the code block contains only a single function with it as an argument, you can use the method reference (::) instead of the lambda expression:


val adam = Person("Adam")
adam.age = 20
adam.city = "London"
println(adam)
2

let is often used to execute code blocks with only non-null values. If you need to perform operations on non-empty objects, can you use safe call operators on them? And call let to perform the action in the lambda expression.


val str: String? = "Hello" 
//processNonNullString(str) //  Compilation error: str  Possible null 
val length = str?.let { 
 println("let() called on $it")
 processNonNullString(it) //  Compile by: 'it'  In  '?.let { }'  The middle must not be empty 
 it.length
}

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


val adam = Person("Adam")
adam.age = 20
adam.city = "London"
println(adam)
4

with

1 non-extension function: The context object is passed as an argument, but inside an lambda expression, it can be used as a receiver (this). The return value is the result of an lambda expression.

It is recommended to use with to call functions on context objects instead of lambda expression results. In code, with can be understood as "for this object, do the following."


val adam = Person("Adam")
adam.age = 20
adam.city = "London"
println(adam)
5

Another usage scenario for with is to introduce a helper object whose properties or functions will be used to calculate a value.


val adam = Person("Adam")
adam.age = 20
adam.city = "London"
println(adam)
6

run

The context object is accessed as a recipient (this). The return value is the result of an lambda expression.

run and with do the same thing, but are called in the same way as let 1--as extension functions for context objects.

run is useful when an lambda expression contains both object initialization and evaluation of the return value.


val adam = Person("Adam")
adam.age = 20
adam.city = "London"
println(adam)
7

In addition to calling run on the recipient object, it can also be used as a non-extension function. Non-extensible run allows you to execute a block of multiple statements where an expression is required.


val hexNumberRegex = run {
 val digits = "0-9"
 val hexDigits = "A-Fa-f"
 val sign = "+-"
 Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
 println(match.value)
}

apply

The context object is accessed as a recipient (this). The return value is the context object itself.

apply is used for code blocks that do not return values and run primarily on members of the recipient (this) object. A common case for apply is object configuration. Such a call can be understood as "applying the following assignment operation to the object".


val adam = Person("Adam")
adam.age = 20
adam.city = "London"
println(adam)
9

With the recipient as the return value, apply can be easily included in the call chain for more complex processing.

also

The context object is accessed as an argument to the lambda expression (it). The return value is the context object itself.

also is useful for performing operations that take context objects as parameters. Use also for operations that require references to objects rather than their properties and functions, or when you do not want to mask this references from external scopes.

When you see also in your code, you can understand it as "and use this object to do the following."


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

Summarize

The following table summarizes the main differences and usage scenarios of Kotlin scoped functions:

函数 对象引用 返回值 是否是扩展函数 使用场景
let it Lambda 表达式结果 1. 对1个非空对象执行 lambda 表达式
2. 将表达式作为变量引入为局部作用域中
run this Lambda 表达式结果 对象配置并且计算结果
run - Lambda 表达式结果 不是:调用无需上下文对象 在需要表达式的地方运行语句
with this Lambda 表达式结果 不是:把上下文对象当做参数 1个对象的1组函数调用
apply this 上下文对象 对象配置
also it 上下文对象 附加效果

References

Kotlin language Chinese network


Related articles: