Kotlin

Kotlin's `let`, `with`, `also`, and `run`: Usage Comparison

Kotlin offers several scope functions: let, with, also, and run. Using these appropriately can make your code more readable and concise. This article explains the characteristics and usage of each scope function.

1. let – Using Variables within a Scope

let is suitable for performing operations within a scope when an object is not null. The receiver (target object) can be accessed using it.

val name: String? = "Kotlin"
name?.let {
    // Name length: 6
    println("Name length: ${it.length}") 
}

Characteristics:

  • Improved null safety (?.let allows for null checks)
  • Accesses the object using it within the scope
  • Facilitates chained operations

2. with – Manipulating Properties within a Context

with is used when performing multiple operations on a specific object. The return value is the result of the last expression in the with block.

val person = Person("Alice", 25)
val info = with(person) {
    "Name: $name, Age: $age"
}
// Name: Alice, Age: 25
println(info)

Characteristics:

  • Accesses object properties using this
  • Executes a series of operations within the context of that object
  • The return value is the result of the last expression

3. also – Applying Side-Effect Operations

also is useful for performing operations with side effects, such as logging or adding debugging information, without modifying the object. The receiver can be accessed using it.

val user = User("John").also {
    // Creating user: User(name=John)
    println("Creating user: $it") 
}

Characteristics:

  • Returns the original object unchanged
  • Applies side-effect operations such as logging or debugging

4. run – For Initialization and Returning Calculation Results

run is suitable for performing initialization while accessing object properties or combining a series of calculations. The receiver can be accessed using this.

val person = Person("Bob", 30).run {
    "$name is $age years old"
}
// Bob is 30 years old
println(person) 

Characteristics:

  • References object properties using this
  • Returns the value of the last expression
  • Enables initialization without using unnecessary temporary variables

Advanced Usage of Kotlin's let, with, also, and run

1. Advanced Usage of let

Example: Transforming a List

This example demonstrates transforming each element in a list and joining them into a string. Using let simplifies the transformation process.

val numbers = listOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map { it * it }.let { it.joinToString() }
// "1, 4, 9, 16, 25"
println(squaredNumbers)

Example: Chained Processing

Applying let sequentially to process a string transformation step by step.

val result = "hello".let { it.uppercase() }.let { "$it World!" }
// "HELLO World!"
println(result)

2. Advanced Usage of with

Example: Comparing Data Class Properties

Using with to easily reference and compare properties of a data class.

data class Person(val name: String, val age: Int)

val person1 = Person("Alice", 30)
val person2 = Person("Bob", 25)

val isOlder = with(person1) {
    age > person2.age
}
// "Alice is older than Bob: true"
println("Alice is older than Bob: $isOlder")

Example: Grouping Initialization Logic

Using with to simplify object initialization.

val paint = with(Paint()) {
    color = Color.RED
    style = Paint.Style.FILL
    this // Returns the Paint instance
}

3. Advanced Usage of also

Example: Copying an Object

Using also to perform side effects while copying an object.

val originalList = mutableListOf("A", "B", "C")
val copiedList = originalList.also { println("Copying list: $it") }.toMutableList()
copiedList.add("D")
// "Original: [A, B, C]"
println("Original: $originalList")
// "Copied: [A, B, C, D]"
println("Copied: $copiedList")

Example: Validation Check

Using also to validate an object's value while continuing processing.

val input = "Hello"
val validatedInput = input.also {
    require(it.isNotEmpty()) { "Input must not be empty" }
}

4. Advanced Usage of run

Example: Applying Configuration

Using run to configure an object efficiently.

class Config {
    var debug = false
    var logLevel = "INFO"
}

val config = Config().run {
    debug = true
    logLevel = "DEBUG"
    this
}
// "Debug: true, LogLevel: DEBUG"
println("Debug: ${config.debug}, LogLevel: ${config.logLevel}")

Example: Handling Nullable Values

Using run to safely process a nullable object.

val result = nullableObj?.run {
    process(this)
}

Summary

  • let is suitable for applying null checks and temporary scopes, accessing the object using it.
  • with is convenient for manipulating object properties collectively, referencing them using this.
  • also is suitable for performing side-effect operations without modifying the object, accessing it using it.
  • run is suitable for object initialization and calculation processing, accessing properties with this.

By appropriately using these scope functions, you can make your Kotlin code more concise and highly readable.