How do visibility modifiers apply to extension functions — what can a private/internal/member extension see and be seen by?
answer
- Modifier applies to the extension, not the extended type
- private top-level = file-only; internal = module; public = everywhere
- Extension sees only receiver's public (+ same-module internal) members
- Member extension = dispatch + extension receiver, can use enclosing privates
- protected only for member extensions, not top-level
basics
~20 sAn extension follows normal visibility rules for where it is declared, not the type it extends. A top-level public extension is usable anywhere; a private one only in its file. Because it sits outside the class, it can only see that type's public members.
solid answer
~50 sExtension *visibility* is governed by where the extension is declared, exactly like any other declaration — `public`, `internal`, `private`, `protected` apply to the extension function itself. A **top-level** extension marked `private` is visible only within its file; `internal` limits it to the module; `public` is everywhere. Crucially, the modifier does **not** change what the extension can *access* inside the receiver: since it is compiled as an outside static method, it sees only the receiver type's `public` (and `internal`, if in the same module) members — never `private` ones. You can also declare an extension as a **member** of another class/object; then it has two receivers (dispatch + extension), its visibility is that of the enclosing class's member, and it can use that class's private state. A common pattern is a `private fun Receiver.helper()` placed at file scope to keep a helper local while still reading like a method.
code
kotlin · 10 lines// File: report.kt
private fun String.indent(n: Int) = " ".repeat(n) + this // file-local helper
internal fun List<String>.report(): String = // module-only
joinToString("\n") { it.indent(2) }
// Member extension with two receivers
class Csv(private val sep: Char) {
fun List<String>.row(): String = joinToString(sep.toString())
}go deeper
Knows public/private apply and that a private top-level extension is file-local.
Explains internal scoping and that the extension only sees the receiver's public API.
Separates callability from access, and describes member extensions with two receivers accessing enclosing privates.
Uses visibility deliberately for API surface control (internal/file-private helpers, member extensions for encapsulated DSL scoping).
## Two separate questions: who can call it vs. what it can see Visibility for extensions splits into: 1. **Where the extension is visible (callable from).** 2. **What members of the receiver the extension can access.** ### 1. Callability follows the declaration site An extension is just a declaration, so the usual modifiers apply to *it*: ```kotlin public fun String.shout() = uppercase() + "!" // visible everywhere internal fun String.tag() = "[$this]" // same module only private fun String.trimQuotes() = trim('"') // this file only (top-level) ``` - **`public`** (default): callable wherever the function is imported. - **`internal`**: callable within the same Gradle/Maven **module**. - **`private`** at top level: callable only within the **same file** — a great way to keep a helper local while reading like a method. - **`protected`** is not allowed for top-level functions (there's no class to be protected within); it's only meaningful for member extensions. ### 2. Access is limited to the receiver's public API Because an extension compiles to a **static method outside the class**, it can read only what any outside caller could: ```kotlin class Account(private val balance: Long) { val isPositive get() = balance > 0 } fun Account.describe() = if (isPositive) "ok" else "low" // fun Account.raw() = balance // ERROR: balance is private to Account ``` It sees `public` members always, and `internal` members if declared in the same module. It can **never** touch `private` members of the receiver type. ## Member extensions: two receivers Declare an extension *inside* a class and it gains a **dispatch receiver** (the enclosing class instance) plus the **extension receiver**: ```kotlin class Formatter(private val sep: String) { fun List<String>.joined(): String = joinToString(sep) // can use Formatter's private sep } ``` Here the extension's visibility is the visibility of that member (`public` by default, but `private`/`protected` to the class are allowed), and it can access the enclosing class's private state because it is *inside* that class. Such member extensions are only callable where both receivers are in scope. ## Practical guidance - Use **`private` file-scope extensions** for local helpers to avoid polluting the public namespace. - Use **`internal`** for module-only utilities. - Remember: marking an extension `public` does **not** grant it access to the receiver's privates — visibility of the extension and access to the receiver are independent.
- Can a public extension function read a private property of the type it extends?No. Being public controls who can call it, not what it can access; a top-level extension only sees the receiver's public (and same-module internal) API.
- Why declare a top-level extension as private?To restrict it to the current file, keeping internal helper functions out of the public namespace while still calling them with dot syntax.
- How does a member extension differ in visibility from a top-level one?It has the visibility of a class member (can be private/protected to the class), gains a dispatch receiver, and can access the enclosing class's private state.
saying these in an interview costs you the question
- Thinking public extensions can access the receiver's private members
- Believing the modifier changes the extended type's visibility
- Saying protected works on a top-level extension
- Not distinguishing 'who can call it' from 'what it can access'
- Unaware of member extensions and their dispatch receiver