Kotlin: run, let, apply, also, with — when to use what? Full breakdown with examples
Hello, friend! If you're learning Kotlin and want to write clean, concise code, you'll sooner or later encounter five magical functions: run, let, apply, also, and with. These are scope functions — they allow you to execute code in the context of an object and greatly simplify your life.
But here's the catch: many beginners (and not only) get confused about when to use which. This question is one of the most popular on Stack Overflow, and today we'll finally figure out how not to make a mistake. We won't just list the rules — we'll show real-world examples from practice that you can immediately use in your projects.
What are scope functions and why are they needed?
Scope functions are functions that create a temporary scope for an object. Inside this scope, you can refer to the object via it or this. The main difference between them is how they return the result and how they refer to the context.
Here's a quick cheat sheet that we'll fill with examples:
let— context:it, returns: lambda result. Used for null checks and call chains.run— context:this, returns: lambda result. A universal function, often used for initialization and computations.apply— context:this, returns: the object itself. Ideal for configuring object properties.also— context:it, returns: the object itself. Good for additional actions (logging, checks).with— not an extension function, context:this, returns: lambda result. Convenient for grouping calls on one object without creating a new one.
Remember the main rule: the choice of function depends on what you need — to return the object or the result, and how you want to refer to the context (via it or this).
1. let — the king of safe chains
let is your best friend when you need to work with nullable objects. It executes the code only if the object is not null. The context is passed as it.
Example: safe transformation
val name: String? = "Andrey"// Without let:if (name != null) { println("Name length: ${name.length}")}
// With let (much more elegant):name?.let { println("Name length: ${it.length}")}let is also great for operation chains. For example, when you need to transform one object into another:
val numbers = listOf(1, 2, null, 4, null, 6)val nonNullSquared = numbers .mapNotNull { it } .let { list -> list.map { it * it } }println(nonNullSquared) // [1, 4, 16, 36]When to use let:
- Null checks (the most common case).
- Transforming one value into another.
- Executing a chain of operations on a result.
2. apply — the constructor without extra words
apply returns the object itself, and inside the lambda the context is this. This is the ideal tool for configuring an object before use. Forget about long set blocks!
Example: configuring a View (Android) or data class
data class User(var name: String = "", var age: Int = 0)
val user = User().apply { name = "Maria" age = 25}println(user) // User(name=Maria, age=25)
// Even cooler — configuring a TextView:val textView = TextView(context).apply { text = "Hello, Kotlin!" textSize = 18f setTextCo