Declarations
distinct fields
Keep same-named fields of different bases in separate slots, reached by qualification.
since 0.1.0-alpha.1linuxwindowswasm
Description
When two bases of a class declare a field with the same name and type, the class must say what it
wants: restating the field gives one slot, and two unrelated declarations are otherwise a compile
error (see lang.multiple-inheritance). Marking a field distinct keeps both declarations apart:
public distinct int value = 0;
A distinct member keeps its own slot, apart from every same-named member of another base. The
slot belongs to the declaration: a member declared once is one slot, however many inheritance paths
bring it into a class. A subclass on that path that redeclares the member overrides the slot. It
changes what the slot holds (a field's initial value, a method's body) and adds no slot, so the slot
always holds the most-derived declaration on its path. Each slot is reached by naming a class on its
path: this.Counter::value, or c.Counter::value from outside.
Two distinct slots with one name
class Counter { public distinct int value = 10; }
class Timer { public distinct int value = 20; }
class Clock : Counter, Timer {
int total() => this.Counter::value + this.Timer::value;
}
Clock c = Clock();
c.Counter::value = 11;
console.writeln(c.Counter::value);
console.writeln(c.Timer::value);
console.writeln(c.total());
11
20
31
Rules
- A collision is two fields of the same name and the same type inherited from different bases.
Without
distinct, two unrelated declarations of one name and type are a compile error; seelang.multiple-inheritance. - If
distinctappears on either side of a collision, the two declarations keep separate slots. Adistinctmember declared once and inherited along several paths is still one slot. - A separate slot is reached through a qualified path
object.Base::name, whereBaseis the class that declares the member or any class that inherits it along that path. A qualified field reads or writes that path's slot. A qualified method call selects the path where the bare call would be ambiguous betweendistinctpaths, and then runs the most-derived body on that path for the receiver's runtime class. Where the bare call is unambiguous, it is a static call ofBase's own body, the way an override calls the method it overrides. Where only one slot of that name is visible — inside the declaring class and its descendants on a single chain, or through a receiver typed as one of them — the bare name reaches it. - A subclass that redeclares an inherited
distinctmember overrides its slot, whether or not it repeatsdistinct. The redeclaration must agree with the member onconst,readonlyandweak. Where the class also inherits a plain member of the same name and type, adistinctredeclaration overrides thedistinctmember and leaves the plain one as it is. A plain redeclaration there would match both members, and a class that inherits the member on two paths has no one path to override, so in both cases the redeclaration is an error:
error: 'hp : int' in 'Leaf' redeclares a `distinct` member that reaches it on two paths, 'Race::hp' and 'Job::hp'; override it in a class on one path instead
- A member written
distinctover an inherited member that is notdistinctis not an override: it asks for a second slot beside the inherited one. - Two unrelated classes that each override the same
distinctmember compete for its one slot. A class that inherits both must redeclare the member to choose:
error: 'hp : int' reaches 'X' through two competing overrides of the `distinct` member 'Race::hp', 'Elf::hp' and 'Dwarf::hp'; declare 'hp' in 'X' to choose
- A bare read of a name that has more than one distinct slot is a compile error that asks you to
qualify the name with
::. - A bare call of a
distinctmethod that is visible on two paths is the same error: the call names no path, so the compiler asks for a qualifier instead of choosing one.
error: ambiguous call of 'hpMod() -> int' (distinct on multiple bases); qualify with '::'
- An accessor is bound to the one field of its name in the class that declares it. A class that holds two fields of that name cannot declare an accessor for it:
error: accessor 'value' in 'Widget' is ambiguous: 'value' reaches 'Widget' as 'Counter::value' and 'value' (distinct on multiple bases); declare the accessor in a class on one path
Examples
distinct on one side only
class A { distinct int v = 1; }
class B { int v = 2; }
class C : A, B { }
C c = C();
console.writeln(c.A::v);
console.writeln(c.B::v);
1
2
An unqualified read is rejected:
An ambiguous bare read
class Counter { public distinct int value = 10; }
class Timer { public distinct int value = 20; }
class Clock : Counter, Timer { }
Clock c = Clock();
console.writeln(c.value);
// error: ambiguous read of 'value : int' (distinct on multiple bases); qualify with '::'
A subclass that redeclares an inherited distinct field on a single chain overrides it, so the
chain behaves as single inheritance:
A single chain behaves as single inheritance
class Race { distinct int hp = 0; }
class Elf : Race { int hp = 2; }
console.writeln(Elf().hp);
2
Each axis of a combined class can redeclare its distinct members. The combined class reaches each
path by qualification: a field reads that path's slot, and a method call runs that path's override:
Redeclaring a distinct member overrides its path
class Race { distinct string name = "?"; distinct int hp = 0; distinct string cry() => "..."; }
class Job { distinct string name = "?"; distinct int hp = 0; distinct string cry() => "..."; }
class Elf : Race { string name = "Elf"; int hp = 2; string cry() => "For the grove!"; }
class Fighter : Job { string name = "Fighter"; int hp = 5; string cry() => "Hold the line!"; }
class Unit : Race, Job {
int maxHp() => 10 + this.Race::hp + this.Job::hp;
string title() => this.Race::name + " " + this.Job::name;
string shout() => this.Race::cry() + " " + this.Job::cry();
}
class Archer : Unit, Elf, Fighter { }
Archer a = Archer();
console.writeln(a.title());
console.writeln(a.maxHp());
console.writeln(a.shout());
Elf Fighter
17
For the grove! Hold the line!
Two classes with the same name from different namespaces can both contribute distinct members.
Inside a namespace the bare qualifier names that namespace's class, and an alias names the other:
Same-named bases from two namespaces
namespace M { class A { distinct int v = 2; string who() => "M::A"; } }
namespace N {
use M::A as MA;
class A { distinct int v = 1; string who() => "N::A"; }
class C : A, MA {
string who() => "C";
int f() => this.A::v;
int g() => this.MA::v;
string w() => this.A::who() + " " + this.MA::who();
void set() { this.A::v = 10; this.MA::v = 20; }
}
}
N::C c = N::C();
console.writeln(c.f().toString() + " " + c.g().toString() + " " + c.w());
c.set();
console.writeln(c.f().toString() + " " + c.g().toString());
1 2 N::A M::A
10 20
Notes
Limits:
- A member declared
distinctonce and inherited along several paths is one slot. Qualifying with different classes on those paths (this.Race::hp,this.Job::hpwhen both derive from the declaring class) names the same slot. - Storage keys and dispatch names use the namespace-qualified class name (
N::A::v), so two classes with the same name from different namespaces may both contributedistinctmembers to one class. Inside a namespace the bare qualifier names that namespace's class; the other is named through an alias (use M::A as MA;thenthis.MA::v).
See also
- Multiple inheritance — Combine several base classes, how same-named fields and methods collapse or stay separate, and how to reach one base's member by qualification.
- Fields — Declare the data a class or struct holds, with optional initializers, and how a field without an initializer gets its default value.