?.let or ?.run, which is better?

I’ve been often using ?.let { ...} to avoid NullPointException(NPE).
For example,

class Dog {
   var skill: Skill? = null
   ...
   fun setSkillFromApi() {
       skill = fetchSkill() // from api and it might be null
       
       skill?.let {
          it.sound = 7
          it.tone = Bark.THICK
       }
   }
   ...
}

The reason why i’ve been using ?.let {...} is just because i saw a lot of sample codes on Internet/Github.

But the it.property seems unnecessary. So I replaced ?.let with ?.run.

class Dog {
   var skill: Skill? = null
   ...
   fun setSkillFromApi() {
       skill = fetchSkill() // from api and it might be null
       
       skill?.run {
          sound = 7
          tone = Bark.THICK
       }
   }
   ...
}

As a result, i can still avoid NPE and also remove the unnecessary it.
But i wonder which way is correct.

2 Likes

let returns the result of the lambda block. run returns Unit. In your case they work the same, but let return value is unused so it is not necessary.

EDIT @kyay10 corrected me. run also returns the result, but takes receiver instead of argument. Mea culpa.

3 Likes

Thank you.
I decompiled the example code and the result of them were definitely same code.
And I have one more question.
It kind of sounds like that i can replace all ?.let with ?.run.
Can i just think of ?.let and ?.run as alternatives?
I’m confused when i use ?.let or ?.run

If you need the result of expression, you use let. If you do not - use run. run is more limited, but it allows to avoid some errors.

1 Like

Could you tell me about the some errors???

You can ask for advice in kotlin slack getting-started channel.

Thank you! Have a good day :slight_smile:

I don’t mean to nitpick, but run doesn’t return Unit, it returns the result of the lambda as well. From the Kotlin docs

 inline fun <R> run(block: () -> R): R

For instance:

fun main() {
  val nullable: String? = "hello"
  println(nullable?.run { 42 })
  println(nullable?.let { 42 })
}
2 Likes

Ah, sorry, my mistake. It takes parameter as a receiver instead of argument and returns the result. It is a bit embarrassing to make such a mistake, but I almost never use it. In this case the difference is only cosmetic.

Using argument instead of receiver is preferred when there is a chance of this clash.

2 Likes

To make things even more complicated, your example can be also written using apply() and also(). There is no correct solution, it depends on the specific case and it is a matter of taste.

Personally, I go by default with functions that return lambda’s value (let()/run()) and use apply()/also() only if required to return the receiver. This is consistent with the behavior of most higher-order functions in Kotlin stdlib as they usually return lambda’s value. Similarly, I go by default with functions receiving a regular parameter (let()/also()) and I use apply()/run() only in specific cases, e.g. if I need to set multiple properties of a single object. Regular parameter is more explicit and I can even provide a name for it. In practice that means I often use let(), sometimes apply() and also() and almost never run(). But this is only me.

3 Likes

I have no strict rules for this and usually use whatever is most readable in a certain context.

For the OP’s case, I usually stick with apply and write this as a single statement:

skill = fetchSkill()?.apply {
  sound = 7
  tone = Bark.THICK
}

I normally stick with apply whenever I just need to change some of the object’s properties, as it seems to me that it’s what apply is for in the first place.

I use also usually when I need to return something but perform some last minute stuff on the object, like:

return calculateResult()?.also {
    this.cachedResult = it
}

I try not to abuse this one, and often just stick to separate statements instead.

I use let the most often of these, usually in a complex expression that can’t be written with a simple ?. safe call like

val name = localName?.let { prefix + it }

If this gets anywhere complicated, I create an extension function and write this instead:

val name = localName?.withPrefix()

And run is the one I use the least. Usually in DSL-like code when I need to call a lot of stuff on a single object and writing its name over and over again gets tedious.

3 Likes

it’s very useful. Thank you!

When i request and get some data from network(Retrofit) or local database(Room), to initialize an object, i use apply() most of time to set object’s properties like your code.
Thank you for letting me know your aspect :wink:

Dart has a nice more concise syntax to some uses of apply that it might be nice if Kotlin supported, but probably won’t since … already had a different meaning. The equivalent code in Dart uses a … cascade operator:

skill = fetchSkill()
  ?..sound = 7
  ?..tone = Bark.THICK

See Language tour | Dart

To answer the original question, neither is better. These are called scope functions and there are actually 5 different ones: run, let, apply, also, and with. The first 4 fill out a 2x2 table of cases whether the value is passed as a parameter or as receiver and whether the result is the original value or the result of the block.

With by the way is just an alternative syntax to run:

    with(x) { ••• }

Is equivalent to

    x.run { ••• }

There are many other reasons to use them besides the null check case. In addition to the ones mentioned in the link I often use them to get a function body to a single line of code so I can define the function with = instead of {}

1 Like

It depends on your use case. If you have nesting of scopes, run can be more confusing. In your example, it can become unclear if sound or tone is coming from current scope or outer scope.

1 Like

You’re right. run could lead to a difficult situation to track this.
Thank you :slight_smile:

scope_fun

3 Likes