Problems with apply function


#1

I had a widget and it’s style need to initialize. Here’s my code with simplification.

class Style() {
    var width = 0.0
    var height = 0.0
    var name = ""
}

class Widget(val name: String, val width: Double) {
    val style: Style
    
    init {
        val height = width * 1.5
        
        style = Style().apply {
            width = width
//            height = height  got a compile error here:Val cannot be reassigned
            name = name
        }
    }
}

I tried to fix it :

class Widget(name: String, width: Double) {
    val style: Style
    
    init {
        val height = width * 1.5
        
        style = Style().apply {
//            width = width  got a compile error here:Val cannot be reassigned
//            height = height  got a compile error here:Val cannot be reassigned
//            name = name  got a compile error here:Val cannot be reassigned
        }
    }
}

Even worse, I tried this:

class Widget(val name: String,val width: Double) {
    val style: Style
    
    init {
        val height = width * 1.5
        
        style = Style().apply {
            this.width = width
            this.height = height
            this.name = name
        }
    }
}

When I called

val widget = Widget("Button", 12.0)
    with(widget.style){
        println("$name, $width, $height")
    }

But result was: , 0.0, 18.0

Finally I tried:

class Widget(name: String, width: Double) {
    val style: Style
    
    init {
        val height = width * 1.5
        
        style = Style().apply {
            this.width = width
            this.height = height
            this.name = name
        }
    }
}

I got the right answer: Button, 12.0, 18.0
I was confused.
Why?


#2

The general rule: your reference is resolved to a local variable or parameter first, then to a property of the nearest this.

You have the same logic in Java: when you want to refer to a field, not a parameter in the constructor body, you write this.height = height. In Kotlin you more often have several implicit this references, like this@apply and this@Widget in your example, but otherwise it’s similar.

That’s why in the first example you try to reassign the local variable and get an error:

val height = width * 1.5

style = Style().apply {
    height = height  // you try to change the local variable declared above
}

By omitting this when you have a property and a constructor parameter with the same name, you refer to a parameter, not a property (as in Java):

class Widget(name: String, width: Double) {
    val style: Style

    init {
        val height = width * 1.5

        style = Style().apply {
            width = width //  both times you refer to the constructor parameter 'width'
            height = height //  you refer to the local variable 'height' 
            name = name  // you refer to the constructor parameter 'name'
        }
    }
}

That means you need to access properties using this reference.

However, in you next example, the second part of the rule comes into play: the reference is resolved to a property of the nearest this. The nearest one is this@apply, so it’s as you wrote the following:

class Style() {
    var width = 0.0
    var height = 0.0
    var name = ""
}
class Widget(val name: String,val width: Double) {
    val style: Style

    init {
        val height = width * 1.5

        style = Style().apply {
           this.width = width // both references are resolved to a property of the nearest this: this@apply
           this.height = height
           this@apply.name = this@apply.name // as you referred to it explicitly by this@apply    
       }
    }
}

You change the mutable variable width by assigning the value 0.0 already stored in it. That’s why you got , 0.0, 18.0 as a result.

In your last example you refer to constructor parameters, so everything works:

class Widget(name: String, width: Double) {
    val style: Style

    init {
        val height = width * 1.5

        style = Style().apply {
            this.width = width   // first 'width' refers to Style property, while second one to constructor parameter
            this.height = height
            this.name = name
        }
    }
}

If you want to make name and width properties of the class Widget, not just the parameters of the constructor, you can refer to them explicitly:

class Widget(val name: String, val width: Double) {
    val style: Style

    init {
        val height = width * 1.5

        style = Style().apply {
            this.width = this@Widget.width  //this works correctly as well
            this.height = height
            this.name = this@Widget.name
        }
    }
}

If you use IntelliJ IDEA, you can always use the navigation and find the declaration your variable or property is resolved to. Declaring several properties and local variables with the same name might indeed be confusing.

If you’re interested in general rules for overload resolution, you can read this description: https://github.com/JetBrains/kotlin/blob/master/spec-docs/NameResolution.adoc