5

The following code will not run:

class X{
 double? x;
}

mixin Y{
 double? y;
}

class Z extends X with Y {
 double? z; 
 Z(this.x, this.y, this.z)
}

The compiler will complain that this.x and this.y are not fields in the enclosing class:

lib/physicalObject.dart:20:10: Error: 'x' isn't an instance field of this class.
Z(this.x, this.y, this.z)
     ^

lib/physicalObject.dart:20:18: Error: 'y' isn't an instance field of this class.
Z(this.x, this.y, this.z)
             ^

Of course this is not true, the fields x and y are inherited from the parent class and mixin. I can use these fields within the child class Z, it seems it is only the constructor, which has a problem accepting these as parameters. But why?

Philipp
  • 499
  • 5
  • 17
  • 1
    What would you expect to happen if `X`'s default constructor initialized `x` too? What if `x` were `final`? – jamesdlin Sep 07 '21 at 02:31
  • very good question, I am not sure, but the same problems arise, when I define the follwoing constructor: Z({ double? x, double? y, double? z}) { this.x = x; this.y = y; this.z = z; } yet, dart lets me do that – Philipp Sep 07 '21 at 02:38
  • 1
    Your last example is well-defined; when `Z`'s constructor body executes, the base class has already been initialized. (And your example would fail if `X.x` were `final`.) See https://stackoverflow.com/a/63319094/. – jamesdlin Sep 07 '21 at 02:52
  • Are you saying that if I write Z(this.z), z is set before the base class has been initialized? I assumed that Z(this.z) and Z(double? z) {this.z = z} were basically different notations for the same thing. – Philipp Sep 07 '21 at 03:10
  • 1
    Yes, that is what I am saying. `Z(this.z)` and `Z(double? z) { this.z = z; }` are not the same since the constructor body can't be used to initialize (non-`late`) `final` fields. – jamesdlin Sep 07 '21 at 03:18
  • The two are not different notations. `Z(this.z);` and `Z(double? z) : this.z = z;` are basically different notations for (almost, but not entirely) the same thing. Assignment inside the body of the constructor is just that - assignment, not initialization. It happens after the object has been created. Initialization happens while the object is being created, and the object cannot be accessed at all until it's entirely initialized. – lrn Sep 07 '21 at 12:06

1 Answers1

6

Dart instance variables (aka. "fields") introduce a storage cell, an implicit getter and an implicit setter (unless the variable is final and not late, then there is no setter).

The constructor initializer can initialize the storage cell directly, without calling the setter, and even if there is no setter. That's what Z(this.z) or Z(double? z) : this.z = z; does, it stores a value directly into the storage of the instance variable.

A constructor like Z(double? z) { this.z = z; } does not store directly into the cell, it instead calls the setter named z= which then might store into the (necessarily non-final or late) variable's storage. Or it might not. Setters are virtual, and a subclass can override them, and the { this.z = z; } assignment would call the overridden setter.

In general, a subclass does not see the variable of the superclass, all it sees is the interface of the superclass, which exposes only the getter and setter. If the superclass decides to change the field declaration into a getter and setter declaration pair, then they can. It's non-breaking, and the subclass can't tell the difference. So, if the subclass can somehow see that a variable can be initialized (not just assigned to), then we have broken that symmetry, and the superclass is locked in to having an initializable variable.

Initialization must happen in the class which declares the instance variable because if it doesn't, we break the abstraction of that class's interface by leaking whether the getter/setter is backed by a field or not, and we then lock the class into that choice. Which is the totally opposite of why Dart has the kind of getter/setter declarations it has, which is to make it an implementation choice whether you use one or the other, not a public API choice.

lrn
  • 64,680
  • 7
  • 105
  • 121