With the introduction of null safety in Kotlin, everyone now knows the standard function let{...}
.
An example is given in the null safety document as follows:
1 2 3 4 5 | val listWithNulls: List<String?> = listOf(“Kotlin”, null) for (item in listWithNulls) { item?.let { println(it) } // prints Kotlin and ignores null } |
Therefore, let will often be interpreted as an alternative to null checking, for example, if (variable! = null)
. More illustration below
1 2 3 4 5 | // Cách tiếp cận thông thường if (variable != null) { /*Do something*/ } // Dường như là cách tiếp cận của Kotlin variable?.let { /*Do something*/ } |
This is allowed, but not always let{...}
When not to use LET
Tip 1.1: When used only to check null for an immutable variable, do not use LET
Imagine if you had a function that could accept a nullable String variable like the example below.
1 2 3 4 5 | // NOT RECOMMENDED fun process(str: String?) { str?.let { /*Do something*/ } } |
But if you try decompiled into Java code, that's it
1 2 3 4 5 6 7 | public final void process(@Nullable String str) { if (str != null) { boolean var4 = false; /*Do something*/ } } |
It implements a variable initialization declaration. When you run this function processing will take a lot more time, it is initializing the variable var4
without any benefit.
If the code above changes to
1 2 3 4 5 6 7 | // RECOMMENDED fun process(str: String?) { if (str != null) { // Do Something } } |
The use of str
in the if
scope is auto-cast
converted to non-nullable
. The decompile code is simpler without adding variables.
1 2 3 4 5 6 | public final void process(@Nullable String str) { if (str != null) { /*Do something*/ } } |
Tip 1.2: If you only want to access modified content that is tested, but not scope content outside the class, do not use LET.
The reason you test a variable is null or not, chances are you want to access its contents.
Suppose you have a webview and want to reset its values if its current value is not null. The following seems very convincing
1 2 3 4 5 6 | // NOT RECOMMENDED webviewSetting?.let { it.javaScriptEnabled = true it.databaseEnabled = true } |
Instead, we will do as below
1 2 3 4 5 6 | // RECOMMENDED webviewSetting?.run { javaScriptEnabled = true databaseEnabled = true } |
Use run
, since it
will be sent as this
variable to scope, and this eliminates the need for it
Tip 1.3: If you need to string the initial variable content after the scope, do not use LET.
Suppose you have a non- null
String
list, and you just want to print its size, and then deepen the string to do something else (for example, let's say you simply want to print each String
). .
One way to do that is
Note: For simplicity's sake, suppose for some reason, I don't want
String
in the inner scope oflet
withit.forEach{ println(it) }
.
1 2 3 4 5 6 | // NOT RECOMMENDED stringList?.let { println("Total Count: ${it.size}") it }?.forEach{ println(it) } |
It's not good, because we have to have it
there for the purpose of taking the return value.
Instead, consider using also
1 2 3 4 5 | // THIS IS BETTER THAN PREVIOUS stringList?.also { println("Total Count: ${it.size}") }?.forEach{ println(it) } |
This will eliminate the need to get it
Note: The better approach above is not ideal (That's why I didn't write
RECOMMENDED
), because it has a lot?
. I will illustrate further below why this is not ideal. But for simple reasons I have to provide examples.
So let
is not the best solution in some cases, though (although we will achieve the goal when using it).
Note: For a better view of other scope functions such as
let
, you can refer to the article below
When to use LET
Tip 2.1: If you are checking the null state of a mutable variable, use LET to make sure the value of that variable is immutable.
This is the case when the status / content of the variable being tested can change even after people have tested it using if condition
See the following global variable example
1 2 3 4 5 6 7 | // RECOMMENDED private var str: String? = null fun process() { str?.let { /*Do something*/ } } |
This makes sense to use let
The reason is because we reward using ìf
to check, smart casting should not be applied to global variables, due to its variability.
1 2 3 4 5 6 7 8 9 | // NOT RECOMMENDED private var str: String? = null fun process() { if (str != null) { println(str?.length) } } |
The above example is clear, even if within the if (str != null)
, we cannot guarantee that str
will not be null. So the operator ?
It is still necessary for us to use it as a conventional approach.
Tip 2.2: If you want to access content outside the scope, use LET
to distinguish it
and this
more easily.
Returning to the webviewSetting
example above, suppose we need access to the external scope variable. If we use run
, it will look like this
1 2 3 4 5 6 7 8 9 | // ERROR var javaScriptEnabled = false var databaseEnabled = false webviewSetting?.run { javaScriptEnabled = javaScriptEnabled databaseEnabled = databaseEnabled } |
This will confuse the compiler with it. Of course you can rename it a bit and the compiler is smart enough to encode them as shown below, and it will compile. However, it is still confusing for PR evaluation, etc.
1 2 3 4 5 6 7 8 9 | // ERROR var isJavaScriptEnabled = false var isDatabaseEnabled = false webviewSetting?.run { isJavaScriptEnabled = javaScriptEnabled isDatabaseEnabled = databaseEnabled } |
So with this, it's better to use below (assuming recommendation 2.1 above is met, in which the variable is subject to change)
1 2 3 4 5 6 7 8 9 | // RECOMMENDED var javaScriptEnabled = false var databaseEnabled = false webviewSetting?.let { javaScriptEnabled = it.javaScriptEnabled databaseEnabled = it.databaseEnabled } |
If using run
, one can use this
, but it's very confusing as shown below in distinguishing which variable belongs to which range.
1 2 3 4 5 6 7 8 9 | // NOT RECOMMENDED var javaScriptEnabled = false var databaseEnabled = false webviewSetting?.run { javaScriptEnabled = this.javaScriptEnabled databaseEnabled = this.databaseEnabled } |
Tip 2.3: When you have a nullable chewy string in front, use LET
to remove multiple ?
check
Refer to the function below
1 2 3 4 5 | // NOT RECOMMENDED fun process(string: String?): List<Char>? { return string?.asIterable()?.distinct()?.sorted() } |
Each chain of ?
is not a good idea, because it will introduce unnecessary null checking conditions, because ?
The first should have eliminated null results.
The reverse code looks like below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Nullable public final List process(@Nullable String string) { List var2; if (string != null) { Iterable var10000 = StringsKt.asIterable( (CharSequence)string); if (var10000 != null) { var2 = CollectionsKt.distinct(var10000); if (var2 != null) { var2 = CollectionsKt.sorted((Iterable)var2); return var2; } } } var2 = null; return var2; } |
Use LET
as below, remove ?
and reduce the function of checking the condition and complexity of the code (it will be helpful if you create unit tests to get higher scores and more complex coverage)
Note: This is with the assumption you need access to external scope content according to point 2 above (note: I removed the outer scope content outside the outer scope for explanation). In other cases, using
run
will be better.
1 2 3 4 5 6 7 | // RECOMMENDED fun process(string: String?): List<Char>? { return string?.let { it.asIterable().distinct().sorted() } } |
Decompiling the code will look like below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Nullable public final List process(@Nullable String string) { List var10000; if (string != null) { int var4 = false; var10000 = CollectionsKt.sorted( (Iterable)CollectionsKt.distinct( StringsKt.asIterable((CharSequence)string))); } else { var10000 = null; } return var10000; } |
Note: In some cases, will the compiler optimize
?
, for example when usingfilter
,map
… In this case, no additional null checking is performed, so the code is decompiled the same. However, withlet
, does it reduce the number?
needed in the code, so it's still handy when we have a long nullable string.
Tip 2.4: When you need to access results, use LET
to return the final result within scope.
Refer to the function below
1 2 3 4 5 6 7 8 9 10 11 12 13 | // NOT RECOMMENDED fun process(stringList: List<String>?, removeString: String): Int? { var count: Int? = null if (stringList != null) { count = stringList.filterNot{ it == removeString } .map { it.length } .sum() } return count } |
We need to get the result of the string, ie the output of sum
.
Although stringList
is an stringList
variable, it seems we should consider not using LET
(Tip 1.1).
In this case, not using LET
will force us to create another variable for that purpose.
Therefore, we can remove additional variables by using LET
as shown below. Be more concise and don't need more variables.
1 2 3 4 5 6 7 | // RECOMMENDED fun process(stringList: List<String>?, removeString: String): Int? { return stringList?.let { it.filterNot{ it == removeString }.map { it.length }.sum() } } |
Hopefully this article will be of much help to you.
Reference source: https://medium.com/@elye.project/kotlin-dont-just-use-let-7e91f544e27f