Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin: Mutual assignments

I want to set up two values that hold immutable references to each other. Example:

data class Person(val other: Person)
val jack = Person(jill), jill = Person(jack)   // doesn't compile

Note: lateinit doesn't seem to work with data class primary constructors.

Any ideas?

like image 369
CSJ Avatar asked Oct 16 '22 17:10

CSJ


2 Answers

You could get away with something like this:

class Person() {
    private var _other: Person? = null

    private constructor(_other: Person? = null) : this() {
        this._other = _other
    }

    val other: Person
        get() {
            if (_other == null) {
                _other = Person(this)
            }
            return _other ?: throw AssertionError("Set to null by another thread")
        }
}

And then you would be able to do:

val jack = Person()
val jill = jack.other

Using a data class here does not work for multiple reasons:

  1. First because a data class can't have an empty constructor.

  2. Even if that wasn't a problem, the generated methods would end up having a cyclic dependency and will fail in runtime with java.lang.StackOverflowError. So you'd have to overwrite toString, equals, etc. which kind of defeats the purpose of using data class in the first place.

like image 63
jivimberg Avatar answered Nov 09 '22 14:11

jivimberg


Here is the trick (note, this is really a trick, you need a good reason to use it in real code).

Unfortunately it won't work with data classes, as they seem to be secured against this kind of hacks.

But if you have java-stile classes, you may use two things to your advantage:

  1. You can initialize vals in the constructor (same as with final in java)
  2. You have access to this inside the constructor (and you may leak it outside if you really want)

Which means that you can create another Person inside the constructor of the first person and finalize the creation of both classes before the constructor finishes.

Once again: exposing this as I did below is a bad idea. When otherFactory is called, it's parameter is only half-initialized. This may lead to nasty bugs, especially if you try to publish such reference in multithreaded environment.

A bit safer approach is to create both Persons inside the constructor of the first Person (you'll need to supply the fields of both entities as arguments). It's safer because you're in control of the code that uses half-initialized this reference.

class Person {
    val name: String
    val other: Person

    constructor(name: String, other: Person) {
        this.name = name
        this.other = other
    }

    // !! not very safe !!
    constructor(name: String, otherFactory: (Person) -> Person) {
        this.other = otherFactory(this)
        this.name = name
    }

    // a bit safer
    constructor(name: String, otherName: String) {
        this.other = Person(otherName, this)
        this.name = name
    }
}

val person1 = Person("first") {
    Person("second", it)
}

val person2 = person1.other

print(person1.name) // first
print(person2.name) // second

val person3 = Person("third", "fourth")
val person4 = person3.other

print(person3.name)
print(person4.name)
like image 30
Aivean Avatar answered Nov 09 '22 13:11

Aivean