The original link

Some of Kotlin’s standard functions are so similar that we are not sure which one to use. Here I will introduce a simple method to clearly distinguish their differences and how to choose to use them.

The scope of the function

I focused on the run, with, t. run, T. et, T. lso and t. ply functions. I call them scope functions because I think their main function is to provide an internal scope for calling functions.

The run function is the simplest scope method to illustrate

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

With this function, you can have a separate scope inside the test function, and the mood is completely enclosed within the run scope before being redefined to I am happy and printed.

This range function by itself doesn’t seem very useful. But another nice thing about scopes is that they return the last object in the scope.

Therefore, the following code will be pure, and we can apply the show() method to two views as shown below, instead of calling it twice.

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

Three properties of the scope function

To make scope functions more interesting, let me classify their behavior with three attributes, and use those attributes to distinguish them.

1. Normal vs. extended functions

If we look at the definition, with and t. run the two functions are actually very similar. The following example implements the same function.

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

However, they differ in that with is a normal function and t. run is an extension function.

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

Imagine if webView.Settings could be empty, it would look something like this.

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

In this case, the t. run extension is obviously better because we can null before using it.

2.This vs. it parameters

If we look at the definitions, t run and t et these two functions are almost identical except for the way they take arguments. The logic of the following two functions is the same.

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

If you check the t.run function signature, you’ll notice that t.run only calls block: t. () as an extension function. So, for all ranges, T can be called this. In programming, this can be omitted most of the time. So, in our example above, we could use $length in the println declaration instead of ${this.length}. I call this passing the this parameter.

However, for t.et function signatures, you’ll notice that t.et passes itself in as an argument, namely block: (T). Therefore, this is like passing a lambda argument. It can use IT as a reference in scope. So I call this passing it parameters.

From above, it seems that t. run is superior because t. et is more implicit, but it is the t. et function that has some subtle advantages as follows:

  • T.letUsing a given variable function/member provides a clearer distinction than using an external class function/member
  • inthisCannot be omitted, for example when it is passed as an argument to a functionitthanthisShorter, clearer.
  • inT.letAllows for better variable names that you can convertitIs another name.
stringVariable? .let { nonNullString -> println("The non null string is $nonNullString")}Copy the code

3. Return current type vs. other type

Now, let’s look at t. et and T.also. If we look at their internal function range, they work the same

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

However, they differ subtly in their return values. T returns a different type of value, while t. also returns T itself, which is this.

Both are useful for linking functions, with t.et you can evolve operations, and with t.also you perform operations on the same variable this.

Here’s a simple example

val original = "abc"
// Change the value and pass it to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // Change the parameter and pass it to the next chain
}.let {
    println("The reverse String is $it") // "cba"
    it.length   // Change the type
}.let {
    println("The length of the String is $it") / / 3
}
/ / error
// Send the same value in the chain (printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // Even if we change it, it won't work
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // Even if we change it, it won't work
}.also {
    println("The length of the String is ${it}") // "abc"
}

// Also can be done by modifying the original string
// Send the same value 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

From above, t.also seems pointless because we can easily combine them into a feature block. But if you think about it, it also has some advantages:

  1. It can provide a very clear separation process on the same object, making smaller functional parts.
  2. Before use, it can achieve very powerful self-manipulation, implementing chain Builder operation (Builder mode).

When the two are combined, a self-evolution, a self-preservation, can become very powerful, as in the following

// The normal method
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// Improve the method
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
Copy the code

All the properties

By illustrating these three properties, we should be able to understand the behavior of these functions. Let’s take a look at the t. ply function again, since it’s not mentioned above. These three attributes are defined in t.apply as follows…

  1. This is an extension function
  2. thethisPassed as a parameter.
  3. It returnsthis(itself)

Therefore, it could conceivably be used as follows

// The normal method
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// Improve the method
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }
Copy the code

Or we can create chained calls.

// The normal method
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
// Improve implementation
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }
Copy the code

Function to choose

So, obviously, with these three properties, we can now classify the above functions accordingly. Based on this, we can form a decision tree below that can help us decide which function to use.


Hopefully, the above decision tree clarifies the differences between functions, simplifies your decisions, and enables you to use these functions appropriately.