Why does the Kotlin compiler forbid a `public inline` function from referencing `private` members of its class, and how does `@PublishedApi` relate to this?
answer
- Inline body runs with caller's visibility
- public inline can't touch private members
- Referenced members must be >= the function's visibility
- @PublishedApi exposes internal to inline code
- @PublishedApi only on internal; affects binary compat
basics
~20 sBecause an inline function's body gets copied into the code that calls it. If that body touched something private, the caller's compiled code would reference a member it isn't allowed to see. @PublishedApi lets you mark an internal member as safe to expose to such inlined code.
solid answer
~50 sAn `inline` function's body is emitted at the *call site*, so it runs with the caller's visibility, not the declaring class's. If a `public` (or `protected`) inline function referenced a `private` member, callers in other classes/modules would end up invoking something they can't legally access — so the compiler rejects it: "Public-API inline function cannot access non-public-API members." Allowed references must themselves be at least as visible as the inline function. The escape hatch is `@PublishedApi`: you annotate an `internal` declaration with it, making it accessible to public inline functions (and thus to the inlined code in other modules) while keeping it out of the normal public API/IDE completion. This is exactly why inlining leaks implementation into consumers and constrains library evolution — you must consciously decide which internals become part of your effective binary surface via `@PublishedApi`.
code
kotlin · 11 linesinternal class Engine // not visible outside module
// ERROR: public inline body can't reference an internal type
// inline fun run(block: (Engine) -> Unit) { block(Engine()) }
@PublishedApi
internal class PublishedEngine // opt the internal into inline use
inline fun run(block: (PublishedEngine) -> Unit) {
block(PublishedEngine()) // now legal in a public inline fun
}go deeper
Recognizes that public inline can't use private members, even if shaky on why.
Explains the call-site-visibility reason and knows @PublishedApi is the workaround for internal members.
Articulates the >=-visibility rule precisely and that @PublishedApi only applies to internal, plus the basic binary-compat implication.
Treats @PublishedApi as a deliberate widening of the binary contract and reasons about library evolution and ABI stability around inline.
## The root cause: body runs at the call site When you call a `public inline fun`, the compiler copies its body into the caller. That copied code executes with the **caller's** access rights. So any member the inline body touches must be visible from wherever the function can be called. Visibility levels, from least to most accessible: `private` < `internal`/`protected` < `public`. A `public inline` function can be called from any module, so its body may only reference members that are also reachable from any module — i.e. effectively `public`. ## The rule A `public` or `protected` inline function **cannot reference declarations less visible than itself** (e.g. `private`, plain `internal`). The compiler errors with something like *"Public-API inline function cannot access non-public-API ..."*. The same applies to `internal inline` referencing `private`. ```kotlin class Cache { private val store = HashMap<String, String>() // ERROR: public inline body would copy a reference to private `store` // into callers that cannot see `store`. inline fun getOrPut(key: String, compute: () -> String): String = store[key] ?: compute().also { store[key] = it } } ``` ## The fix: `@PublishedApi` `@PublishedApi` marks an `internal` declaration as *part of the published API for inlining purposes*. It becomes accessible to public inline functions (and therefore to the inlined copies that land in other modules) but is hidden from normal IDE completion / treated as not-quite-public for everyday use. ```kotlin class Cache { @PublishedApi internal val store = HashMap<String, String>() // exposed only for inline use inline fun getOrPut(key: String, compute: () -> String): String = store[key] ?: compute().also { store[key] = it } // now OK } ``` Note you can only put `@PublishedApi` on `internal` members; it would be redundant on public and illegal on private. ## Why this is a tradeoff, not just a rule Because the inline body is baked into every consumer's compiled output, anything it references (including `@PublishedApi` internals) becomes part of your **effective binary contract**: - Renaming or changing the signature of a `@PublishedApi` member can break already-compiled callers (binary incompatibility) until they recompile. - It widens your maintenance surface: 'internal' is no longer truly private. So the visibility rule + `@PublishedApi` are the language making the cost of inlining explicit: you must opt internals into the public binary surface deliberately, rather than leaking them by accident.
- Can you annotate a private member with @PublishedApi?No. @PublishedApi applies only to internal declarations. Private isn't supported, and on public members it's redundant/an error.
- What's the binary-compatibility risk of @PublishedApi members?Their references are inlined into consumers' bytecode, so renaming or changing their signature can break already-compiled callers until they recompile — they're effectively part of your binary API.
- Does an internal inline function face the same restriction?Yes, relative to its own visibility: an internal inline function still cannot reference private members, because those references would be copied where private isn't accessible.
saying these in an interview costs you the question
- Saying the rule is arbitrary instead of a visibility/access consequence
- Thinking @PublishedApi can be applied to private members
- Not realizing inlined code carries the caller's access rights
- Ignoring that @PublishedApi widens the binary contract
- Confusing @PublishedApi with @JvmStatic or @Suppress