Custom getter/setter for properties created by type params


#1

Hi!

I’m just starting out with kotlin and i was trying to build a simple data class with custom getters/setters. What i want would look like this in Java:

public class JavaPerson {

  private final String name;

  private int age;

  public JavaPerson(final String name, final int age) {

  super();

  this.name = name;

  this.age = age;

  }

  // default name getter, no setter

  // default age getter

  public void setAge(final int age) {

  System.out.println("Setting age to " + age);

  this.age = age;

  }

}

However i struggle to get the same thing working in kotlin:

public class Person(val name: String, var age: Int) {

  override fun setAge(age: Int) {

          println("Setting age to " + age);

          this.age = age

  }

  // doesn't compile: overrides nothing

  public fun setAge(age: Int) {

          println("Setting age to " + age);

          this.age = age

  }

  // fails at compile and/or runtime: java.lang.ClassFormatError: Duplicate method name&signature in class file de/itso/kotlintest/Person

}

I’ve also tried to do it like this:

public class Person private constructor (val name: String) {

  private var age: Int

  public constructor(name: String, age: Int) : this(name) {

          this.age = age

  }

}

But this fails, as i have to initialize age when i declare it, although i assign it in the only public constructor. I would consider this a workaround, even if it compiled.

Can anybody help me here?


#2

If you need a custom getter or setter for your property, you cannot declare it in the primary constructor parameter. You need to declare a property in the body of the class.

class Person(val name: String, age: Int) {
  var age: Int = age
  set(value) {
  println(“Setting age to $value”)
  field = value;
  }
}


#3

I see! I was under the impression that this was a redeclaration, as the property exists as a type param as well as as a property. Unfortunately the current eclipse plugin seems to treat this as an error, as it can't resolve 'field'.


#4

How can we deal with this when working with data classes?

For data classes all primary constructor parameters need to be marked as val or var. So when I want to have a special setter for one of my (required/non-optional) data class properties, I have to implement equals(), hashCode() toString() etc. on my own or is there another cool way?


#5

Any news on the @mikrobi question?


#6

You can use a backing property in this situation:

data class C(private val _propertyWithSetter: String) {
    val propertyWithSetter: String
        get() = _propertyWithSetter
        set(value) {
            /* execute setter logic */
            _propertyWithSetter = value
        }
}

#7

Thanks, I think that should go in the wiki :slight_smile:


#8

But the name of constructor argument starts with “_” is quite a problem. (eg. named argument call, copy, …)
And if we have many properties many boilerplate will made.

I think there should be a better way for ‘private set applied to constructor var’.


#9

The compiler is actually smart enough to do the right thing for the following code:

class person(name:string) {
    var name:String = name
      private set

  // Rest of the class
}

#10

I Agree. Clean constructor and property expression.

But not for data classes.


#11

I ran into the exact same problem.


#12

I’m facing something very similar, in which I need to modify a parameter on its initial construction, and would prefer to use a data class. In particular, I am writing a Direction class for a three-dimensional unit vector using the Apache Commons Math library Vector3D; the norm of the vector should always be one. What I wish I could write is something like:

data class Direction(val vector: Vector3D set(value) { field = vector.normalize() } ) { ... }

As it is I am not using a data class, and thus am re-implementing my own equals(), hash(), …


#13

I have similar problem. Firebase cant parse my data class completly. It can parse all params except for one var is_captain:String? = null

I tried to write custom get and set as mentioned above
data class Member(var _is_captain: String? = null) {
{
var is_captain: String?
get() = _is_captain
set(value) {
_is_captain = value
}
}

but it still throws ClassMapper: No setter/field for is_captain found


#14

Hi! I found one behavior I don’t understand:

class User( nameParam: String ) {
    var name : String = nameParam  // why is nameParam saved directly into name.field here ?!?
        set(value) {
            println("Set is invoked")
            field = value.toUpperCase() + "!!!"
        }
}

fun main(args: Array<String>) {
    val ivan = User("Ivan")  // <<- setter is not invoked here!
    println(ivan.name)

    ivan.name = "Ivan" // <<- but now it is invoked 
    println(ivan.name)
}

Result of execution I expect:

Set is invoked
IVAN!!!
Set is invoked
IVAN!!!

BUT I SEE THIS:

Ivan
Set is invoked
IVAN!!! 

(tested on https://try.kotlinlang.org on all available Kotlin’s versions)


#15

Yes, that’s correct. The initializer of a property is stored directly into a backing field, without invoking the setter.


#16

Thank you for explanation. This behavior is not very clear though. I guess you use it to initialize internal field, right?
So, to achieve my aim I want to use init {} block:

class User(nameParam: String) {
  var name : String = <init_value>
     set(value) { ... }
  init {
    name = nameParam
  }
}

#17

It does in fact say in the documentation that the initializer value is written directly to the backing field.

Consequently, if you do want the setter to be called, you have to do this in an init block as you’re now doing.

However, an unsatisfactory aspect of doing this is that you still need to provide an initial value for the backing field which is therefore, in effect, being initialized twice. You can’t, of course, use lateinit if there’s a custom setter.

Whilst the choice of initial value isn’t really a problem for primitives or String (you can always use "" for the latter), it could be a problem for other types and you may therefore have to resort to making the property nullable.

I’m not aware of any way to avoid this double initialization.


#18

What about the lazy delegate?


#19

Can’t you have a proxy function like this:

class User( nameParam: String ) {
    var name : String = transformValue(nameParam)
        set(value) {
            field = transformValue(value)
        }

}

private fun transformValue(value: String): String {
    println("Set is invoked")
    return value.toUpperCase() + "!!!"
}

It might not be as elegant, but I don’t see why it wouldn’t work. It can of course be a method as well.


#20

@tieskedh

Although there are some similarities, the lazy delegate is really trying to solve a different problem to what we have here.

@ilogico

Whilst your approach should work, I think it might be a case of the cure being worse than the disease!

It dawned on me after making my previous post that, if you’re going to call the setter in init, then you might as well always initialize the property to exactly the same value. So there isn’t really a problem choosing a suitable initial value even for non-primitive types.

The double initialization problem remains (as, frankly, it does in many other OO languages) though at the end of the day it’s not going to cost us many clock cycles :slight_smile: