Reading — step 1 of 7
Learn
Extension Functions
You cannot edit String. It ships inside the standard library, and the method you
wish it had is not there. Kotlin's answer is to let you write that method anyway,
outside the class, and call it as though it had always belonged:
fun String.shout(): String = this.toUpperCase() + "!"
fun main() {
println("ship".shout())
//> SHIP!
}
String. before the name is the receiver type — the type you are extending.
Inside the body, this is the value the function was called on. No subclassing, no
wrapper, no edit to String.
Grader note: this course runs Kotlin 1.3.70. The modern spelling
"ada".uppercase()does not exist yet and fails to compile withunresolved reference: uppercase. UsetoUpperCase()here.
It is a function, not a method
That distinction is the whole mental model, and it explains everything surprising
about extensions. The compiler rewrites "ship".shout() into an ordinary static
call with "ship" as the first argument. Nothing is added to String at run time.
Two consequences follow, and both bite people.
Consequence one: resolution is static
The extension that runs is chosen from the declared type of the variable, not the object actually in it:
open class Animal
class Dog : Animal()
fun Animal.name(): String = "animal"
fun Dog.name(): String = "dog"
fun main() {
val a: Animal = Dog()
println(a.name())
val d: Dog = Dog()
println(d.name())
//> animal
//> dog
}
a holds a Dog, and still prints animal. A member function would have printed
dog — overriding is a run-time lookup, extensions are not. If you need
polymorphism, you need a real member.
Consequence two: a member always wins
If the class already has a matching function, the member is called and your extension is dead code:
class Box { fun hello(): String = "member" }
fun Box.hello(): String = "extension"
fun main() {
println(Box().hello())
//> member
}
The compiler warns you — extension is shadowed by a member: public final fun hello() — which is worth reading rather than scrolling past.
Nullable receivers
The receiver type may be nullable, and then the null check moves inside the function. This is the one extension shape that genuinely cannot be written any other way:
fun String?.orUnknown(): String = if (this == null || this.isBlank()) "unknown" else this
fun main() {
val missing: String? = null
println(missing.orUnknown())
println("ada".orUnknown())
//> unknown
//> ada
}
Note what is not there: no ?., no !!, no if (x != null) at the call site.
missing.orUnknown() is a plain dot on a nullable value, and it is safe, because
this being null is a case the function itself handles.
Common mistakes
- Expecting an extension to override a member — the member wins, silently apart from a warning.
- Expecting polymorphism — the declared type picks the extension, so a
Dogin anAnimalvariable gets theAnimalone. - Reaching for private state — an extension is an outside function; it sees only the public surface of the type.
- Forgetting the receiver can be nullable —
fun String?.f()handles null inside, which is usually cleaner than making every caller write?..
Your exercise
Initials asks for both shapes at once: a plain extension and a nullable one.
Write fun String.initials(): String, turning "ada lovelace" into "A.L." — the
uppercased first letter of each word, each followed by a dot. Split on
Regex("\\s+"), not on " ", so a run of several spaces still counts as one gap.
Write fun String?.orUnknown(): String, returning "unknown" when the receiver is
null or blank, and the receiver itself otherwise.
The driver is written for you: it reads a count, then that many lines, treating a
line of - as null. For each it prints the name followed by -> and that name's
initials — so - prints unknown -> U., because "unknown" is what initials()
then receives. Use toUpperCase(), not uppercase().
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…