Recently I placed some validation logic into a custom setter and expected that this logic would be called when the object gets instantiated. But this is not the case – it is only called when the property value is changed after construction.
The following example shows that the setter is only called once
class Demo(x: Int) {
var x = x
set(value) {
println("field: $field, value: $value")
field = value
}
}
fun main() {
val d = Demo(1)
d.x = 2 // setter only called here
}
There is another topic in this forum showing a nice solution. But what is the reason the setter is not called on object construction? Is there a deeper reason? Is that something that should be improved in the language (principle of least astonishment …)?
I’m not really sure why this is the case. One reason is that you can read the old value in the setter which can lead to NPEs during construction since there is no value yet.
When I need validation logic I prefer to call it as part of the init block. Something like this is an alternative:
class Demo(x: Int) {
var x: Int = -1 // setter not called, we need a value for the not initialized case
set(value) {
println("field: $field, value: $value")
field = value
}
init {
this.x = x // this calls the setter
}
}
fun main() {
val d = Demo(1)
d.x = 2
}
I’m not sure, maybe. It is a surprising fact about kotlin and thus can lead to many bugs, but on the other hand it is valid to have setters that require the class to be properly initialized. Both scenarios can lead to bugs where code expects the class to be “valid” but has to handle “invalid” data.
This should however be documented and I can’t find this in the documentation. Well it kinda is documented here as part of a comment in the example. Not sure that is good enough.
I’ve used the init block at first, but that doesn’t solve all problems since the value is actually set twice then. In cases where the former value is relevant, like in a validation where the new value must be greater than the existing value, it simply doesn’t work with a setter + init block.
you don’t want the setter called in the constructor so you can start with any value => just use a normal property initialization setting the field directly
you want a “min value” so you set the field to that value.
But how is that setter supposed to work without an initial value?
Option 2 is probably acceptable in most cases, but it is still a bit counter intuitive that the setter is not called in my my first example where the property is set directly (var x = x). However, since the previous value doesn’t matter in the case of the object construction, setting a default value would be ok. But then again: why doesn’t the compiler just set field to a reasonable default value like null, 0 or false then and initialize the field with the setter on construction?
I have no idea why the kotlin team decided to do this, but I guess one reason is that a variable of type T does not have a good default value. It’s not nullable so null does not work, but you can’t use anything else either.
But as I mentioned above, maybe it’s due to the fact that setters might require the object to be properly initialized, so calling the setter would create bugs that way.
I don’t think there is a perfect solution to this problem.
I would say the biggest reason is that when the property is set the object is not fully initialised (it cannot be guaranteed that calling the setter is valid). Imagine a setter that invokes change listeners. If the list of change listeners is declared later in the source than the setter the change list (even if a non-nullable list) will actually have a null value and looping over all listeners will throw a null pointer exception. There are many other things that can be validly done to a fully initialised object, but not to a partially initialised object.
I created an issue here for this to be documented more clearly. I’m not sure how many people will actually read the docs, but I guess it might help some people.
Also KT-6624 confirms that this is intended behavior.
This is a very common pattern I use in Python. It uses the setter’s validation to initialize the object:
class Demo:
def __init__(self, x: int) -> None:
self.x = x # This calls the setter
@property
def x(self) -> int: # Getter
return self._x
@x.setter
def x(self, value: int) -> None: # Setter
# Implement validation here
self._x: int = value
After trying a few things, IntelliJ proposed the following, and it seems to be working:
class Demo(x: Int) {
init {
x.also { this.x = x } // This calls the setter
}
var x: Int // We don't need a value for the setter
set(value) {
println("field: $field, value: $value")
field = value
}
}
If we don’t use the also {} scope function, we should declare the property before the init {} block:
class Demo(x: Int) {
var x: Int = -1 // Setter not called, we need a value for the not initialized case
set(value) {
println("field: $field, value: $value")
field = value
}
init {
this.x = x // this calls the setter
}
}