Translation description:

原 文 : Mastering Kotlin Standard functions: Run, with, let, also and apply

Original address:medium.com/@elye.proje…

Author:Elye

Some of the library functions in Kotlin are so similar that we’re not sure which one to use. Here I’ll show you a simple way to clearly distinguish the differences between them and how to choose which function to use.

Scoped function

Next, I’ll talk about the functions run, with, T.run, T.lie, T.lily, and T.ply, and call them scoped functions because I notice that their main function is to provide an internal scope for the caller function.

The easiest way to illustrate scope is with the run function

fun test(a) {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}
Copy the code

In this example, there is a separate scope inside the test function that completely contains an operation implementation where the mood variable is redefined and initialized to “I am Happy” before output.

This scoped function by itself may not seem very useful. But what’s more interesting about this than scope is that it returns something, which is the last object inside that scope.

As a result, the following would be even cleaner, as we can apply the show() method to two views without having to call the show() method twice.

run {
        if (firstTimeView) introView else normalView
}.show()
Copy the code

Three property characteristics of a scoped function

To make scope functions more interesting, let me classify their behavior into three property characteristics. I will use these property characteristics to distinguish each of these functions.

Normal vs. Extension functions

If we compare the two functions with and t run, they are actually quite similar. Here is an example of using them to achieve the same functionality.

with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}
// similarly
webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}
Copy the code

However, the only difference between them is that with is a normal function, while t. run is an extension function.

So the question is, what are the advantages of each?

Imagine if webView. Settings might be empty. How do you modify it in the following two ways?

// Yack! (Ugly implementation)
with(webview.settings) {
      this? .javaScriptEnabled =true
      this? .databaseEnabled =true}}// Nice.webview.settings? .run { javaScriptEnabled =true
    databaseEnabled = true
}
Copy the code

In this case, the t. run extension function is obviously better because we can check for nullability before using it.

2, this vs. it argument (this vs. it argument)

If we compare t. ray and t. let, the two functions are very similar, the only difference is that they take different parameters. The same logic for both functions is shown below.

stringVariable? .run { println("The length of this String is $length")}// Similarly.stringVariable? .let { println("The length of this String is ${it.length}")}Copy the code

If you look at the t.run function declaration, you’ll notice that t.run is only treated as a block of calls to the block: t. () extension function. Therefore, in its scope, T can be referred to by this. In most cases this can be omitted during coding. So in our example above, we can use $length in the println statement instead of ${this.lenght}. So I’m going to call this passing this argument

For the declaration of T et, however, you’ll notice that T et is passed itself to the function block: (T). So this is similar to passing a lambda expression as a parameter. It can be referred to within the scope of a function using it. So I’m going to call this passing it parameters

From above, it may seem that T. ray is superior to T.let because it is more implicit, but the t. let function has some subtle advantages, as follows:

  • The t. let function provides a clearer way to distinguish between the functions/members of a given variable and the functions/members of an external class.
  • 2. For example, when it is passed as an argument to a function, this cannot be omitted, and it is more concise and clear to write than this.
  • 3. T.let allows for better naming of converted used variables, i.e. it can be converted to other meaningful names, whereas t.run cannot. Internally, it can only be referred to or omitted by this.
stringVariable? .let { nonNullString -> println("The non null string is $nonNullString")}Copy the code

Return this vs. other type (Return this vs. other type)

Now, let’s look at T et and t so, and if we look at its inner function scope, they’re all the same.

stringVariable? .let { println("The length of this String is ${it.length}")}// Exactly the same as belowstringVariable? .also { println("The length of this String is ${it.length}")}Copy the code

However, they differ subtly in that their return value, T.lett, returns a different type of value, while T.letso returns the t-type itself, which is this.

Both functions are useful for chaining calls to functions, where t. let you evolve operations and t. so lets you perform operations on the same variables.

Here’s a simple example:

val original = "abc"
// Evolve the value and send to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // can be evolve to other type
}.let {
    println("The length of the String is $it") / / 3
}
// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}
// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain 
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") / / 3
}
Copy the code

A lso may seem pointless because we can easily combine them into a functional block. If you think about it, it has some very good advantages.

  • 1. It provides a very clear separation of the same objects, creating smaller parts of the function.
  • 2, before use, it can be very powerful self operation, so as to achieve the entire chain of code to build operations.

When the two are used together, a self-evolution, a self-preservation, it can make some operations more powerful.

// Normal approach
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
Copy the code

Review all attribute characteristics

By reviewing these three property characteristics, we can get a good idea of the behavior of the function. Let me explain the t.ply function, which I didn’t mention above. The three attributes of t.ply are as follows

  • 1. It is an extension function
  • 2. It is passed this as an argument
  • 3. It returns this (itself)

So, using it, you can imagine that it could be used as:

// Normal approach
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// Improved approach
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }
Copy the code

Or we can have an unchained object create a chained call.

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }
Copy the code

Selection of functions

So, obviously with these three attribute characteristics, we can now classify functions accordingly. Based on this, we can build a decision tree below to help determine which function we want to use to select the one we need.

Hopefully, the above decision tree will clarify the features and simplify your decisions so that you can properly grasp the use of these features.

Translator has something to say

  • 1. Why am I translating this blog?

We all know that there are less than 100 lines of library functions in the Standard.Kt file in Kotlin, but they are so powerful that they can be said to run through the entire Kotlin development coding process. Using them makes your code more readable, elegant, and concise. Good at reasonable use of standard library functions, is also a measure of your mastery of Kotlin standards, because you go to see some open source Kotlin source code, everywhere you can see is the use of various standard library functions.

However, the difficulty with these library functions is that their usage is very similar. Some people even think that some library functions are redundant. In fact, each library function has its own practical application scenario. While sometimes you can do the same thing with a library function, that may not be the best way to do it. I believe that many beginners are also confused about these standard library functions (I used to be), But this blog is very little because it extracts the three main features: the library function whether extension function, whether this or it as a parameter (inside the function is this and refer to it), whether to need to return to the caller object itself, can be classified based on the characteristics, classification after the corresponding application scenario would be clear at a glance. This good at extracting features of the train of thought is worth learning.

  • 2, about the use of library functions need to add a few points.

First point: it is recommended that you do not use multiple library functions to nest, do not simplify to simplify, otherwise the whole code will be greatly reduced readability, after a while, you may not even know what to refer to.

The second point: The let and run functions are able to return other types of values because the inner block lambda expression returns type R. The return type of the let and run functions depends on the type of the block lambda expression passed in. However, determining the return type of a block lambda expression depends on passing the return value of the last line inside the lambda expression

Third point: the reason why both the t.also and t.ply functions return themselves is because they call return this on the last line inside each Lambda expression, returning themselves. This can refer to the caller because they are both extension function features

  • 3, summarize

This article is to inform the application scenarios and clarify their differences, and use library functions to simplify the implementation of code to master, do not abuse or your code will be poor readability, the following will be deep into the internal principles of each library function, welcome to pay attention to.

Welcome to the Kotlin Developer Alliance, where you can find the latest Kotlin technical articles. We will translate one foreign Kotlin technical article every week. If you like Kotlin, please join us ~~~