JavaRush/Courses/Kotlin SELF/Extension properties

Extension properties

Open

1. Introduction

When you write a learning project (for example, our console expense tracker), the code quickly grows lots of small “conveniences”: normalize a command here, make a nice label for an amount there, compute “the last index of a string, but safely”. All of this can be implemented as functions — and that’s fine. But sometimes, semantically, you want a property, because we’re not “doing an action”, we’re “looking at a characteristic” of an object: expense.title, money.formatted, command.normalized.

Extension properties help make code more “readable at a glance”. They’re especially nice where you format output, build reports, do simple checks and transformations. You just have to remember: this is not magic and not “adding fields to an object”: Kotlin does not modify the original classes; we simply get a more convenient access syntax.

2. How extension properties work: why this is not a field

An extension property looks like a property, but internally it’s just a pair of accessor functions (getter/setter). So it has no “place inside the object” where you could store a value. Kotlin is honest: want an extension property — sure, but only via get()/set(). And that’s why you can’t write = 123 as an “initialization” — the compiler forbids it.

The basic forms look like this:

val String.normalized: String
    get() = trim().lowercase()

var StringBuilder.indent: Int
    get() = 0 // example, but pointless
    set(value) { /* ... */ }

And a useful “mental model” (very rough, but works for understanding):

obj.someProp
   ↓
getSomeProp(obj)  // roughly: a generated getter function

You can even draw a mini diagram:

flowchart LR
    A["obj.someProp"] --> B["someProp.get()"]
    B --> C["get() code runs"]
    C --> D["a value is returned"]

To avoid confusion, keep a simple rule in mind: if you can think of it as “a value computed right now”, an extension property fits. If you need to “remember a value in the object and retrieve it later”, an extension property does not fit.

Below is a small comparison table to reinforce the difference between a regular class property and an extension property:

What we compare Regular property in a class Extension property
Has a backing field (internal storage field) May have one (field) No
Can you write = ... as initialization Yes No, forbidden
Can it be a computed property Yes Yes
Can it store state “in the object” Yes No (only external tricks)
Access to private fields of the class Yes No

3. Examples: strings and data models

Normalizing command input

In console apps, half the “pain” is user input. A user enters " ADD ", "Add", "add", sometimes even "aDd" (why do we love them again?). So string normalization is eternal.

In previous lectures we would do this as a function. But here you can afford a property, because “the normalized version of the string” is a characteristic of the string, not an “action that changes something”.

val String.normalizedCommand: String
    get() = trim().lowercase()

fun main() {
    val raw = "  AdD   "
    println(raw.normalizedCommand) // add
}

This reads almost like “the string has a normalized command”. But remember: the string does not change — we just compute the result each time.

Another classic example is “the last index, but safely”. For an empty string, length == 0, and length - 1 yields -1. That’s fine if that’s what you intended (for example, to avoid indexing altogether).

val String.lastIndexOrMinusOne: Int
    get() = length - 1

fun main() {
    println("".lastIndexOrMinusOne)       // -1
    println("kotlin".lastIndexOrMinusOne) // 5
}

Here an extension property is basically “documentation in code”: you see the name lastIndexOrMinusOne and immediately understand what will happen for an empty string.

Formatting Money and output strings for Expense

As a project grows, you almost inevitably introduce your own types. In an expense tracker it’s convenient to have a “money” type, so you don’t confuse “francs/dollars” with “internal kopeks/cents”, and you don’t accidentally add strings to numbers.

Let’s create a minimal money type via a value class (we already know them from the type-safety block), and move formatting into an extension property. Important: formatting is a “representation”; it does not change the object, so a property is appropriate here.

@JvmInline
value class Money(val cents: Long)

val Money.formatted: String
    get() {
        val sign = if (cents < 0) "-" else ""
        val abs = kotlin.math.abs(cents)
        val dollars = abs / 100
        val rest = abs % 100
        return "$sign$dollars.${rest.toString().padStart(2, '0')}"
    }

fun main() {
    println(Money(12345).formatted)  // 123.45
    println(Money(-90).formatted)    // -0.90
}

Yes, this computation is not “free” (there’s abs and padStart). But it’s lightweight enough and expected: when you write money.formatted, you’re basically asking for “a string representation”. That’s normal.

Now let’s add an expense model and make a nice string for a list. Again: we don’t want to clutter a data class with formatters (often you want to keep the data model simple), so an extension property is a good compromise.

data class Expense(
    val title: String,
    val amount: Money,
    val category: String
)

val Expense.listLine: String
    get() = "${category.padEnd(10)} | ${title.padEnd(15)} | ${amount.formatted}"

fun main() {
    val e = Expense("Coffee", Money(390), "food")
    println(e.listLine) // food       | Coffee          | 3.90
}

Pay attention to the “honesty” of the name. listLine explicitly says: this is a line for output. If we named it val Expense.info, it would be less clear what the format is and where it’s used.

4. var extension properties and external storage

With val extension properties everyone is usually happy: they compute and return. But var raises a natural question: “If I implement set(value), where does it get written?” And this is where you should stop and mentally ask: “Am I sure I’m not trying to attach a field to the object?”

Kotlin allows var extension properties, but reminds you: they have no backing field, so by themselves they don’t store anything. If you need to “as if attach a value”, you’ll have to store it somewhere outside (usually in a Map).

Let’s show this with an example very similar to the one in the Kotlin documentation. Suppose we want to add a “tag” to an expense (for example, "coffee", "home", "urgent"), but we don’t want to (or can’t) change Expense right now.

We’ll create external storage and an extension property that looks into it. To make the example more stable, we’ll key by id, not by the object instance. (Note: for the example, a simplified version of Expense is used here.)

data class Expense(
    val id: Int,
    val title: String
)

private val tagsById = mutableMapOf<Int, String>()

var Expense.tag: String?
    get() = tagsById[id]
    set(value) {
        if (value == null) tagsById.remove(id) else tagsById[id] = value
    }

fun main() {
    val e = Expense(id = 1, title = "Coffee")

    println(e.tag) // null
    e.tag = "drink"
    println(e.tag) // drink
}

Technically, everything works. But design-wise this is already “thin ice”: Expense looks like it has a tag field, but in reality the tag lives in a separate map. This can be an acceptable temporary solution, but it’s easy to turn into a source of strange bugs (for example, if id gets reused or if you forget to clear tagsById).

5. Limitations: initialization, heavy get(), and side effects

Why you can’t initialize an extension property

Sometimes a beginner thinks like this: “Alright, since there’s no field, I’ll just create it. I’ll write var X.y = 0 and done.” Kotlin replies: “Initializers for extension properties are forbidden.” The reason is simple: an initializer implies storing a value, but there’s nowhere to store it — we can’t physically add a field to someone else’s object.

Here is the “wrong dream” that won’t compile:

data class User(val name: String)

var User.age = 0 // WILL NOT compile: no get/set and no field

If you really want User to have an age field, the correct options are usually: add the property to the class itself, create a wrapper around the object, or honestly store the state in external storage (for example, in a repository/map) without pretending it’s “a field inside”.

And yes, you can hide external storage behind a var extension property (as in the example above). But that’s a conscious trick, not “the normal way to add a field”.

Heavy get() and unexpected effects

There’s a subtle psychological thing: when you see obj.prop, your brain expects it to be fast and surprise-free. But when you see obj.calculateTotal(), your brain is prepared that there may be computation. So a property with a heavy get() can become a trap for the reader — and for you a month later.

Let’s show this using an expense list example. It seems tempting to write this:

@JvmInline
value class Money(val cents: Long)

data class Expense(val amount: Money)

val List<Expense>.total: Money
    get() {
        var sum = 0L
        for (e in this) sum += e.amount.cents
        return Money(sum)
    }

Formally, this is correct. But now expenses.total is a full pass over the list every time. If you access this property ten times in a loop, you do ten passes. For a learning project with small data, that’s survivable, but it’s a dangerous habit.

In such cases it’s often better to choose an extension function (which we already know how to write), so the call name more honestly warns about computation. Just because Kotlin allows a property doesn’t mean it’s always better.

Even worse is when get() has side effects. For example, it prints or mutates a collection. That turns obj.prop into a “tiny horror story”: you merely read a value, and the app suddenly changed something or printed to the console.

6. Where to keep extension properties in a project

When extensions become useful, it’s tempting to create a file named Extensions.kt and dump everything there: strings, money, reports, kittens. This quickly turns into a graveyard of random functions where finding anything is impossible (like the Downloads folder, only worse).

Kotlin conventions suggest a pretty sensible idea: extension functions/properties that matter “for all clients” of a class are better kept near that class. And those needed for a specific scenario (for example, only for CLI output) should live near that scenario, not globally.

For our learning expense tracker, you can think of it like this: the Expense.listLine format is specifically console output, so it makes sense to keep it near the CLI code or in a formatting file. But Money.formatted, if it’s used in different places, makes sense to keep near Money or in a single “financial” module of the project.

And one more practical point: extension properties are brought in via imports, and name conflicts are very real. The more precise and domain-specific names you choose (listLine, formatted, normalizedCommand), the lower the chance you’ll accidentally create two val String.value in different packages and then play “guess the import”.

7. Typical mistakes when working with extension properties

Mistake #1: trying to “bolt on a field” via var X.prop = ....
This is a common reflex after languages where you can add dynamic fields or have metaprogramming. Kotlin doesn’t work that way: an extension property cannot have an initializer, because that implies storing a value in the object, and we do not change the object. The right strategy is to implement get/set and understand where the state will actually live.

Mistake #2: a “heavy” get() disguised as a simple property.
If get() does noticeable work (iterates a large collection, builds a long string, reads a file), the code reader may not expect that cost. As a result, you get unexpected slowdowns and duplicated computations. In such cases it’s usually more honest to choose a function so the syntax itself suggests “this is computation”.

Mistake #3: side effects in get(): printing, mutation, hidden changes.
Properties in Kotlin read like “took a value”. If your get() calls println(), changes a global variable, or modifies a collection, the code becomes unpredictable. Such things are better expressed as functions with descriptive names, so the action is explicit.

Mistake #4: var extension property that “seems to store”, but actually stores in a non-obvious place.
The MutableMap trick is sometimes needed (as a temporary adapter), and Kotlin even shows a similar example in the documentation. But if it’s not obvious to the code reader where the value lives and who owns the lifecycle of that storage, you end up with hard-to-debug issues. If the property should semantically be part of the model, most often it’s better to add a regular property to the class.

Mistake #5: expecting access to private class fields from an extension property.
An extension property does not become “part of the class” and does not gain privileges. It sees only what is visible from the outside. If you hit private, that’s a signal: either you need a public method/property in the class itself, or you’re trying to violate encapsulation and should rethink the design.

1
Task
Kotlin SELF,  38 level,  1 lecture
Locked
Terminal command
Terminal command
1
Task
Kotlin SELF,  38 level,  1 lecture
Locked
Last character
Last character
1
Task
Kotlin SELF,  38 level,  1 lecture
Locked
Checkout price tag
Checkout price tag
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet