Compare designing an extension with a nullable receiver `fun T?.foo()` versus a non-null receiver called via `?.foo()`. What are the semantic and API-design differences?
answer
- Nullable receiver → always runs, body owns null
- `?.` short-circuits → null in, null out
- Nullable receiver can return non-null type
- Name clearly: orEmpty / isNullOr...
- Static resolution by declared type
basics
~20 sA nullable-receiver extension runs even when the value is null and decides what null means. With a non-null receiver and ?., the whole call is skipped when null and the result is null. They behave differently for null inputs.
solid answer
~40 sWith `fun T?.foo(): R`, the function is **always invoked**, even on null, and the body defines null's meaning (e.g. `isNullOrEmpty()` returns `true` for null). With `fun T.foo(): R` called as `value?.foo()`, the safe call **short-circuits**: if `value` is null the call is skipped and the expression evaluates to `null` (type `R?`), so the function never sees null. Design implications: use a nullable receiver when null has a meaningful, total result (a default, a boolean, an empty collection) and you want a non-null return type; use a non-null receiver + `?.` when 'no value' should propagate as null. Nullable receivers also let you keep call sites clean (`name.orEmpty()` vs `name?.let { it } ?: ""`). Stdlib follows this: `isNullOrEmpty`, `orEmpty`, `Any?.toString` take nullable receivers deliberately; most others are non-null and you compose with `?.`.
code
kotlin · 8 linesfun List<Int>?.sumOrZero(): Int = this?.sum() ?: 0 // always defined, returns Int
fun List<Int>.firstOr(d: Int): Int = firstOrNull() ?: d // non-null receiver
fun main() {
val a: List<Int>? = null
println(a.sumOrZero()) // 0 (function ran on null)
println(a?.firstOr(-1)) // null (safe call short-circuited)
}go deeper
Knows ?. skips the call on null while a nullable-receiver extension still runs.
Explains the differing result types (null-propagating vs non-null) and picks an approach for a simple case.
Gives a clear decision guide, cites stdlib precedents, and accounts for static resolution and chain-breaking.
Frames null-handling as an API contract decision affecting downstream nullability, readability, and surprise; sets naming conventions for a codebase.
## Two strategies for null ### A. Nullable receiver — the function owns null ```kotlin fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank() val s: String? = null println(s.isNullOrBlank()) // true — function ran, decided null means "blank" ``` - The function is **always called**, even on null. - Return type can be **non-null** (`Boolean`, `String`) because the body produces a value for null. - Call site stays flat: `s.isNullOrBlank()`. ### B. Non-null receiver + safe call — the caller owns null ```kotlin fun CharSequence.firstChar(): Char = this[0] val s: String? = null val c: Char? = s?.firstChar() // null — call skipped entirely ``` - `?.` **short-circuits**: null in ⇒ null out, function never runs on null. - Result type becomes **nullable** (`Char?`). - The caller must keep handling null downstream. ## Decision guide | Want… | Choose | |-------|--------| | A total result defined for null (default/empty/boolean) | nullable receiver | | Non-null return type even when input is null | nullable receiver | | 'No value' should propagate as null | non-null receiver + `?.` | | Function logic that is meaningless for null | non-null receiver | ## Subtle points - **Resolution is static**: `s.foo()` picks the extension by the *declared* type of `s`. If both a `T.foo()` and a `T?.foo()` are in scope, the receiver's nullability of the expression determines which applies; a non-null value can call the nullable-receiver one (null is just never the input). - **Chaining**: a nullable-receiver extension that returns non-null breaks an unwanted null-propagation chain — `list.orEmpty().map { ... }` never NPEs. - **Readability vs hidden behavior**: nullable receivers hide null handling inside; that's great for `orEmpty` but can surprise readers if the null semantics are non-obvious. Name it clearly (`...OrEmpty`, `isNullOr...`). ## Example contrast ```kotlin // Nullable receiver: always defined fun List<Int>?.sumOrZero(): Int = this?.sum() ?: 0 // Non-null + safe call: caller deals with null fun List<Int>.average2(): Double = sum().toDouble() / size val avg: Double? = nums?.average2() ```
- If both `fun String.foo()` and `fun String?.foo()` are imported, which is called on a non-null `String`?Both are applicable, but the non-null `String.foo()` is the more specific receiver and wins overload resolution for a non-null expression; the nullable one applies when the expression's type is nullable.
- Why does a nullable-receiver extension help break null-propagation chains?It can return a non-null type (e.g. `orEmpty(): List<T>`), so subsequent calls in the chain operate on a guaranteed non-null value and never need further `?.`.
Nullable receiver is a vending machine that gives a default snack when you insert nothing; ?. is a turnstile that simply won't let an empty-handed person through.
saying these in an interview costs you the question
- Claiming `?.foo()` still runs the function on null
- Saying nullable receiver must return a nullable type
- Ignoring naming so null semantics are hidden/surprising
- Confusing static extension resolution with virtual dispatch
- Believing the two designs behave identically for null inputs