12

How can I solve the following case?

interface I
class A(i: I)
class C : I, A(this) // << --- 'this' is not defined in this context

In short, I want to pass the class instance to super class constructor.
Is it possible in Kotlin?

P.S. All the answers are good and technically correct. But let's give a concrete example:

interface Pilot {
   fun informAboutObstacle()
}

abstract class Car(private val pilot: Pilot) {
    fun drive() {
        while (true) {
            // ....
            if (haveObstacleDetected()) {
                pilot.informAboutObstacle()
            }
            // ....
        }
    }
    fun break() {
        // stop the car
    }
}

class AutopilotCar : Pilot, Car(this) { // For example, Tesla :)
    override fun informAboutObstacle() {
        break() // stop the car
    }
}

This example don't look too contrived, and why can't I implement it with OOP-friendly language?

Alexey
  • 2,980
  • 4
  • 30
  • 53
  • 1
    This is probably a better fit for composition, than inheritance. Let your `C` maintain an instance of `I` instead of _being_ one. Alternatively you could allow the class `A` to have its `I` provided through a setter, instead of at initialization-time. โ€“ Craig Otis Jan 25 '18 at 20:32
  • And if it has to be inheritance, you could use a secondary zero-arg constructor and check if this implements I? โ€“ Paul Hicks Jan 25 '18 at 21:00
  • I think your expanded example doesn't add anything substantial to the problem statement. You still have this inconvenient fact that you make `Car` refer to itself as if it was another collaborator object. I don't see any benefit in modeling `Autopilot` as both a `Pilot` and a `Car`. โ€“ Marko Topolnik Jan 28 '18 at 20:45

2 Answers2

8

No, this is not possible on the JVM. this is only available after the super class has been initialized.

From

https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.10.2.4

The instance initialization method (ยง2.9.1) for class myClass sees the new uninitialized object as its this argument in local variable 0. Before that method invokes another instance initialization method of myClass or its direct superclass on this, the only operation the method can perform on this is assigning fields declared within myClass.

So the bytecode instruction aload 0 to push this on the stack is forbidden before the super-class constructor is called. That's why it cannot be passed as an argument to the super-constructor.

Kotlin was born as a JVM language and aims for maximum interoperability with Java code and a minimum overhead of its language features. While Kotlin could have chosen to orchestrate object initialization in a different way, it would create problems in mixed Java-Kotlin class hierarchies and add significant overhead.

Ingo Kegel
  • 46,523
  • 10
  • 71
  • 102
2

In the good tradition of OOP languages such as Java, C# or Swift, Kotlin doesn't allow you to leak the this reference before the call to superclass initialization has completed. In your special case you're just storing the reference, but in just a slightly different case the superclass code might try to use the received object, which at that point is still uninitialized.

As a specific example of why languages don't allow this, consider a case where A is a class from a library you use and this rule is not in effect. You pass this like you do and things work fine. Later you update the library to a newer version and it happens to add something as benign as i.toString() to its constructor. It has no idea it's actually calling an overridden method on itself. Your toString() implementation observes all its invariants broken, such as uninitialized vals.

This design suffers from other problems, not just the circular initialization dependency you are struggling with now. In a nutshell, the class A expects this:

A and I

But instead you create this:

C is I

The class A has a dependency on a collaborator object of type I. It doesn't expect itself as the collaborator. This may bring about all kinds of weird bugs. For example your C.toString() may delegate to super.toString() and A.toString() (A is the super of C) may call into I.toString(), resulting in a StackOverflowError.

I can't say from your question whether A is designed for extension, which would make the C : A part correct, but you should definitely disentangle A from I.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436