You maintain a published Kotlin library. What policy would you adopt for `inline` functions on your public API, and why?
answer
- Public inline body = part of the ABI
- Consumers recompile to get fixes
- Inline only for reified / non-local / measured hot path
- Thin inline shim delegates to evolvable impl
- Budget @PublishedApi; run binary-compat validator
basics
~20 sUse public inline sparingly. Because the body is copied into every user's compiled code, changing it later can break them without warning. Reserve inline for tiny helpers that truly need it, and keep the body stable.
solid answer
~50 sFor a published library, a public `inline` function's body becomes part of the **binary (ABI) contract**: it's compiled into every consumer's bytecode, so consumers don't pick up implementation changes until they recompile, and altering the body or any `@PublishedApi` member it touches can break already-compiled callers. My policy: (1) only make functions inline when they genuinely need it — `reified` type parameters, non-local returns from lambdas, or measured hot-path lambda overhead — not by default; (2) keep inline bodies small and stable, delegating real work to non-inline functions so the implementation can evolve; (3) minimize `@PublishedApi` internals since each one widens the binary surface; (4) use binary-compatibility tooling (e.g. the Kotlin binary-compatibility-validator) and treat inline body changes as potential ABI breaks; (5) prefer non-inline plus `noinline`/`crossinline` only where semantics require. The guiding principle: inline trades flexibility and binary stability for a perf/feature gain, so spend it deliberately.
code
kotlin · 10 lines// Thin, stable inline shim frozen into consumers...
inline fun <reified T : Any> Bundle.get(key: String): T? =
getOrNull(key, T::class.java)
// ...real logic kept in an evolvable @PublishedApi internal:
@PublishedApi
internal fun <T : Any> Bundle.getOrNull(key: String, type: Class<T>): T? {
// implementation can change across releases without breaking callers
return readRaw(key)?.let { type.cast(it) }
}go deeper
Understands at a high level that inline copies code so changing it later is risky.
Recommends limiting public inline and gives the recompile-to-get-fixes reasoning.
Proposes thin-shim delegation and limiting @PublishedApi, with the ABI rationale.
Articulates a full policy including binary-compat tooling, when inline is justified, documentation, and treats inline as an ABI/contract decision.
## Why public `inline` is a contract decision When you publish `inline fun foo(...)`, the compiler emits no callable shared method for consumers to link against in the normal way — instead, **foo's body is copied into each consumer's compiled code**. Consequences for a library: - **Stale implementations**: bug fixes or behavior changes in an inline body don't reach users until *they* recompile against the new version. - **ABI fragility**: changing the body, its signature, or any `@PublishedApi` member it references can break binary compatibility with code already compiled against the old version. - **Leaked internals**: anything the body touches must be at least as visible as the function, forcing you to expose internals via `@PublishedApi` — widening the surface you must keep stable. ## A concrete policy ### 1. Inline only when justified Legitimate reasons: - **`reified` type parameters** (impossible without inline), - **non-local return / control flow** through the lambda, - **measured** allocation/dispatch overhead in genuinely hot paths. If none apply, leave it non-inline. The compiler's no-lambda warning is a hint. ### 2. Keep inline bodies thin Make the inline function a small shim that delegates to a regular (non-inline, possibly `@PublishedApi internal`) function holding the real logic: ```kotlin inline fun <reified T> decode(json: String): T = decodeImpl(json, T::class.java) // tiny, stable inline shim @PublishedApi internal fun <T> decodeImpl(json: String, type: Class<T>): T { /* evolvable */ } ``` Now you can change the real implementation freely; only the thin shim is frozen into consumers. ### 3. Budget `@PublishedApi` Each `@PublishedApi internal` is effectively public for binary purposes. Keep the count low, name them stably, and never change their signatures lightly. ### 4. Enforce with tooling Run a binary-compatibility checker (the official Kotlin **binary-compatibility-validator**) in CI; treat changes to inline bodies and `@PublishedApi` members as ABI-affecting in review. ### 5. Document the guarantee State which inline functions are stable and that consumers should recompile on upgrade. ## The principle Inline buys performance and language features at the cost of **flexibility and binary stability**. On a published API those costs are paid by everyone downstream, so inline is a deliberate, documented choice — not a default.
- How do you change an inline function's behavior without breaking compiled consumers?Keep the inline part a thin, stable shim that delegates to a non-inline (or @PublishedApi internal) function, and put the changeable logic there so the frozen part never moves.
- What tooling helps catch inline-related binary breaks?The Kotlin binary-compatibility-validator (kotlinx) tracks the public ABI, including inline signatures and @PublishedApi members, and fails CI on unintended changes.
- Is removing `inline` from a published function a safe change?Not necessarily — it changes codegen and can break non-local returns/reified usages at call sites and is an ABI-relevant change for consumers; treat it like any binary-compat decision.
A public inline function is like a contract clause stamped into every signed copy: you can't quietly amend it — every counterparty holds the old text until they re-sign.
saying these in an interview costs you the question
- Defaulting public APIs to inline 'for performance'
- Unaware inline body is frozen into consumers
- Adding @PublishedApi members casually
- No binary-compatibility process for a published library
- Assuming a published bug fix in an inline body reaches users automatically