Extension Functions & Properties Interview Questions
KOTLIN › Types & Classes
When would you choose an extension function over adding a method to a class you own, and where's the line where that becomes an anti-pattern, e.g. burying business logic in scattered extension files?
What a strong answer covers: Extensions earn their keep for utility or convenience behavior on types you don't own, like Android SDK classes or third-party library types, or for keeping a core model class free of concerns that don't belong to its central responsibility, like formatting or UI-adjacent helpers. It becomes an anti-pattern when core business logic that belongs to a type's identity gets scattered across many loosely organized extension files with no discoverability, since extensions don't show up in a type's own member list the way real methods do, making the actual behavior of a class hard to reason about from the class itself.
Android KTX is full of extensions like .dp, .observe, viewModels(). What's actually happening under the hood that makes these 'free', and where would that assumption break down?
What a strong answer covers: Extension functions compile to ordinary static methods taking the receiver as the first parameter, so calling 8.dp or liveData.observe(...) has no more runtime cost than calling a static helper directly, there's no subclassing and no virtual dispatch overhead beyond a plain function call. The 'free' assumption breaks down when the extension itself does real work internally, like viewModels() which is a lazy-backed property delegate wrapping ViewModelProvider construction; that's not free, it just hides real allocation and lookup cost behind a clean one-line call.
You're designing a Kotlin DSL using lambda-with-receiver (T.() -> Unit). Walk me through why you'd choose that over a plain lambda parameter, and what's the risk if the receiver type has ambiguous or shadowed names in nested DSL blocks?
What a strong answer covers: A lambda-with-receiver makes the DSL block's members directly callable without a prefix, since this is implicit, which is what gives builder DSLs like buildString {} their clean declarative look, versus a plain lambda parameter where every call would need an explicit receiver reference. The risk in nested DSL blocks is implicit receiver shadowing: an inner block's receiver hides the outer one's members of the same name, so a call can silently resolve to the wrong scope; the standard fix is @DslMarker, which makes the compiler reject ambiguous unqualified access across nesting levels unless you explicitly qualify it with this@Outer.