skip to content

Explain how buildString works under the hood: what is the receiver inside its lambda, why is it inline, and how does it compare to constructing a StringBuilder manually?

level: middleimportance: should knowfreq 55%

answer

  1. inline fun buildString(builderAction: StringBuilder.() -> Unit): String
  2. = StringBuilder().apply{...}.toString()
  3. Lambda with receiver: 'this' is the builder
  4. inline -> no lambda alloc, non-local return
  5. Overload with capacity to pre-size

basics

~10 s

buildString makes a StringBuilder for you, runs your code with that builder as this so you can just call append, and returns the finished string. It's basically StringBuilder().apply{...}.toString() wrapped up neatly.

solid answer

~40 s

`buildString` is an `inline fun` in the Kotlin stdlib. Its signature is roughly `inline fun buildString(builderAction: StringBuilder.() -> Unit): String`. The parameter type `StringBuilder.() -> Unit` is a **function literal with receiver**, so inside the lambda `this` is the `StringBuilder` and you call `append`, `insert`, `deleteAt`, etc. without any qualifier. It creates a `StringBuilder()`, applies your `builderAction` to it, and returns `toString()`. Functionally it equals `StringBuilder().apply { ... }.toString()`. Because it is `inline`, both the function body and your lambda are inlined at the call site: no lambda object is allocated and there is no extra call overhead. There is also an overload taking an `Int` initial capacity: `buildString(capacity) { ... }`, which pre-sizes the internal array to avoid resize copies when you know the rough output length.

code

kotlin · 5 lines
kotlin
val report = buildString(64) {            // pre-sized
    append("User: ").append(name).appendLine()
    for ((k, v) in attrs) append(k).append('=').append(v).appendLine()
}
// equivalent to StringBuilder(64).apply { ... }.toString()

go deeper

for a junior

Knows buildString gives a builder and returns a String, and can use append inside it.

for a middle

Explains the receiver lambda, that it equals apply+toString, and uses appendLine and the capacity overload.

for a senior

Articulates why inline removes allocation, enables non-local return, and ties it to the broader buildX/@DslMarker receiver-lambda pattern.

for a principal

Weighs scoping/readability vs. an explicit long-lived builder, and reasons about when pre-sizing capacity is worth it based on profiling and allocation budgets.

## Signature and shape The stdlib declares (simplified): ```kotlin public inline fun buildString(builderAction: StringBuilder.() -> Unit): String = StringBuilder().apply(builderAction).toString() public inline fun buildString(capacity: Int, builderAction: StringBuilder.() -> Unit): String = StringBuilder(capacity).apply(builderAction).toString() ``` ## Function literal with receiver The parameter type is `StringBuilder.() -> Unit` — a **lambda with receiver**. That dot before `()` means the lambda runs *as if it were a method on* `StringBuilder`: inside it, `this` refers to the builder, so `append(x)` is really `this.append(x)`. This is the same mechanism `apply` and Kotlin DSLs (`@DslMarker`) use. ```kotlin val csv = buildString { // 'this' is a StringBuilder append("id"); append(',') append("name") appendLine() } ``` ## Why inline matters `inline` tells the compiler to splice both the function body and the lambda directly into the caller. Consequences: - **No allocation** of a `Function`/lambda object — zero-overhead abstraction over the raw builder. - **No virtual call** for the builderAction. - `return` inside the lambda can be a **non-local return** from the enclosing function (a property of inline lambdas). ## vs. manual StringBuilder ```kotlin // Manual val sb = StringBuilder() sb.append("a"); sb.append("b") val s1 = sb.toString() // buildString val s2 = buildString { append("a"); append("b") } ``` They are equivalent in performance. `buildString` wins on readability: the builder is **scoped** to the lambda (can't leak or be used after `toString()`), and the `toString()` is automatic. Prefer it unless you need the builder to outlive a single block. ## Pre-sizing capacity If you can estimate output size, `buildString(estimatedLen) { ... }` constructs the backing array large enough up front, avoiding intermediate doubling/copy steps — a useful micro-optimization for hot paths. ## Related: buildList / buildSet / buildMap / StringBuilder.appendLine Kotlin has the same `buildX { }` pattern for collections. Inside `buildString` you also get `appendLine()` (appends content plus the line separator) as a convenient extension.

  • Could you `return` from the outer function inside a buildString lambda?
    Yes — because buildString is inline, a bare `return` is a non-local return from the enclosing function. A labeled `return@buildString` would just exit the lambda.
  • When would you NOT use buildString and keep an explicit StringBuilder?
    When the builder must outlive a single block — e.g., passed across helper functions, accumulated conditionally over a longer scope, or returned as a builder.

buildString is a self-cleaning workbench: it hands you the tool, lets you build, then automatically wraps the result and clears the bench.

saying these in an interview costs you the question

  • Thinking buildString returns a StringBuilder, not a String
  • Not knowing `this` is the receiver inside the lambda
  • Believing the lambda allocates an object despite inline
  • Confusing it with String.format or templates
  • Unaware of the capacity overload

context