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?
answer
- inline fun buildString(builderAction: StringBuilder.() -> Unit): String
- = StringBuilder().apply{...}.toString()
- Lambda with receiver: 'this' is the builder
- inline -> no lambda alloc, non-local return
- Overload with capacity to pre-size
basics
~10 sbuildString 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 linesval 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
Knows buildString gives a builder and returns a String, and can use append inside it.
Explains the receiver lambda, that it equals apply+toString, and uses appendLine and the capacity overload.
Articulates why inline removes allocation, enables non-local return, and ties it to the broader buildX/@DslMarker receiver-lambda pattern.
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