7.1.18. Classes and Inheritance
This tutorial covers defining classes, constructors, super() and
super.method(), abstract and virtual methods, inheritance with
override, polymorphism, private members, and static fields/methods.
7.1.18.1. Defining a class
Classes are like structs with virtual methods and inheritance:
class Shape {
name : string
def Shape(n : string) {
name = n
}
def abstract area : float
def describe {
print("{name}: area = {area()}\n")
}
}
7.1.18.2. Inheritance and super
Single-parent inheritance with class Derived : Base.
Use super() to call the parent constructor:
class Circle : Shape {
radius : float
def Circle(r : float) {
super("Circle") // calls Shape`Shape(self, "Circle")
radius = r
}
def override area : float {
return 3.14159 * radius * radius
}
}
Use super.method() to call the parent’s version of a method,
bypassing virtual dispatch:
class Rectangle : Shape {
width : float
height : float
def Rectangle(w, h : float) {
super("Rectangle")
width = w
height = h
}
def override describe {
super.describe() // calls Shape`describe(self)
print(" (w={width}, h={height})\n")
}
}
The compiler rewrites super() to Parent`Constructor(self, ...) and
super.method() to Parent`method(self, ...).
7.1.18.3. Implicit super() chain — no constructor needed
If the parent has a user-defined constructor that’s callable with no
arguments, the compiler synthesizes a default constructor for the derived
class that chains super() automatically:
class Animal {
def Animal { print("Animal init\n") } // 0-arg user ctor
}
class Pet : Animal {
name : string = "rex"
}
new Pet() // prints "Animal init" — synth ctor calls super()
The synthesis runs only when:
the parent has a user-defined constructor (any signature),
the derived class has no constructor of its own (none at all), and
the parent ctor is 0-arg-callable (no arguments, or all arguments have defaults).
If the derived class defines its own constructor — even one that only
takes arguments — the auto-generated 0-arg ctor falls back to plain
field-init (preserving the new Class(field=val) named-init idiom).
The lint catches missing super(...) in user-defined ctors on every
control-flow path, so the user-ctor path always runs the parent’s
invariants. new Class() (no args) on such a class continues to call
the field-init synth — it does not run the user ctor.
If the parent has only constructors that require arguments, the derived class must declare its own constructor:
class A { def A(val : int) { /* ... */ } }
class B : A {} // ERROR: no 0-arg parent ctor to chain
class B : A { def B { super(0) } } // OK
7.1.18.4. super(...) is required exactly once per path
Every constructor in a derived class whose parent has a user-defined constructor
must call super(...) exactly once on every control-flow path. The lint catches:
class Bad : Base {
def Bad(f : bool) {
if (f) {
super() // ERROR: false branch has zero super() calls
}
}
}
class AlsoBad : Base {
def AlsoBad {
super()
super() // ERROR: super() called more than once
}
}
A call to super(...) inside a loop is also rejected — the count is not
bounded to one. To branch on arguments, both branches must call super(...):
class OK : Shape {
def OK(big : bool) {
if (big) {
super("Big")
} else {
super("Small")
}
}
}
7.1.18.5. super.X() cannot skip an intermediate
super.X() (whether X is a method or the name of an ancestor class) may
walk past only intermediate classes that have no user-defined constructor or
matching method. Skipping a class whose user code would otherwise establish
invariants is rejected. Empty intermediates are still legal:
class A {
def A { /* ... */ }
}
class B : A {
def B { super() } // B has its own ctor
}
class C : B {
def C {
super.A() // ERROR: would silently skip B's invariants
}
}
If you really want only A’s setup, name the immediate parent:
super.B() (or super()) and let B’s constructor chain to A itself.
7.1.18.6. Creating instances
Use new to create a class on the heap:
var c = new Circle(5.0)
c.describe()
7.1.18.7. Polymorphism
Base pointers hold any derived instance. Method calls dispatch virtually:
var shapes : array<Shape?>
shapes |> push(new Circle(3.0))
shapes |> push(new Rectangle(2.0, 5.0))
for (s in shapes) {
s.describe() // calls the correct area() for each type
}
7.1.18.8. Private members
class Counter {
private count : int = 0
def increment { count += 1 }
def get_count : int { return count }
}
7.1.18.9. Static fields and methods
static fields are shared across all instances.
def static methods access static fields but not self:
class Tracker {
static total : int = 0
id : int
def Tracker {
total += 1
id = total
}
def static getTotal : int {
return total
}
def static reset {
total = 0
}
}
Call static methods with backtick syntax:
print("{Tracker`getTotal()}\n")
Tracker`reset()
7.1.18.10. Key concepts
abstract— subclasses must implementoverride— replaces a parent methodsealed— prevents further overriding or inheritanceself— implicit pointer to current instancesuper()— calls parent constructorsuper.method()— calls parent’s version of a methoddef static— method without self, accesses static fields onlyClassName`method()— call static methods from outside
Note
Structs can also have methods via the [class_method] annotation
from daslib/class_boost, which adds an implicit self parameter
to static functions on structs.
Next tutorial: Generic Programming