Skip to Content

Class

class is a composite type with reference semantics: assignment and argument passing share the same object, so changing an alias changes the original. Classes support single inheritance, virtual methods, abstract and static members, and are the primary declaration form for data that carries behavior and identity.

Declaration and Construction

class Animal { public var name: string = "?"; init(n: string) { name = n; } public virtual func Speak() -> string => "..."; } class Dog : Animal { public var tricks: int = 0; init(n: string) : base(n) { } public override func Speak() -> string => "woof"; }

Constructors are written init, triggered through new, and cannot be called directly. : base(n) chains to the base-class constructor; omitting the chain is equivalent to : base(). virtual declares a method that may be overridden, and override supplies a new implementation in a derived class; dispatch follows the runtime type.

Reference Semantics

val d = new Dog("Rex"); val alias = d; alias.tricks = 5; print(d.tricks);
5

alias and d are the same object. This contrasts with the struct’s value copy: when you need an independent copy, the class must provide its own Clone-style method (see the Self example in Protocol).

Constructor Delegation and base

Constructors delegate to same-class overloads with : init(...) and chain to the base class with : base(...); omitting the chain is equivalent to : base():

class Widget { public val serial: int; public var label: string = "standard"; init(serial: int, label: string) { self.serial = serial; self.label = label; } init(serial: int) : init(serial, "standard") { } } class Gadget : Widget { public var watts: int = 0; init(s: int) : base(s, "appliance") { watts = 5; } } val w = new Widget(1); print(w.serial, w.label); val g = new Gadget(2); print(g.serial, g.label, g.watts);
1 standard 2 appliance 5

new Widget(1) walks the delegation chain to fill in the default label; Gadget’s init sets the label to “appliance” through base.

Primary Constructors and the Overload Set

The primary constructor (class Name(args) { }) promotes its parameters to private val fields and joins the constructor overload set: if the argument count matches an explicit init, that init runs; otherwise, if the arguments match the primary constructor’s parameter shape, the primary-constructor path runs. When an explicit init and the primary constructor share the same parameter shape, the explicit init wins:

class Pack(n: int) { public var shown: string = "unset"; init(n: int) { shown = "explicit"; } } class Crate(n: int) { public var shown: string = "primary"; init() { shown = "zero-arg"; } } print(new Pack(1).shown); print(new Crate(7).shown); print(new Crate().shown);
explicit primary zero-arg

The primary constructor has no chaining syntax for now; omitting the chain follows the “implicit : base()” rule.

Types with an explicit constructor also carry a definite-assignment contract: a val instance field without an initializer must be assigned within the union of assignments across every constructor (including : init(...) delegation chains), or the compiler reports an error. var fields are exempt thanks to their zero value, and so are types without an explicit constructor (they go through the object-initializer path).

Fallible Construction

When an init carries a fail clause, the new expression itself is fallible and the call site must consume it; with try? catching the failure, it produces null:

error BadCode(code: int); class Vault { public val code: int; init(code: int) fail BadCode { if code < 0 { fail new BadCode(code); } self.code = code; } } val v = try? new Vault(9); print(v?.code); val bad = try? new Vault(-1); print(bad == null);
9 True

The failure escapes level by level along the construction and delegation chain. Calling fallible constructors inside field initializers is forbidden.

Polymorphism

A base-class reference holds a derived object, and virtual methods dispatch by the actual type:

val zoo = new list<Animal>(); zoo.Add(new Animal("_GENERIC_")); zoo.Add(new Dog("Rex")); for pet in zoo { print(pet.Speak()); }
... woof

Shadowing vs. Overriding

When the base-class method is not marked virtual, a same-named method in the derived class shadows rather than overrides:

class Plain { public func Go1() -> string => "base"; public virtual func Go2() -> string => "base"; } class Shadowed : Plain { public func Go1() -> string => "derived"; public override func Go2() -> string => "derived"; } val d: Plain = new Shadowed(); print(d.Go1()); print(d.Go2());
base derived

For polymorphism, mark the base method virtual and the derived one override. If you truly want shadowing (an unrelated method that happens to share the name), rename as the warning suggests, or acknowledge and ignore it.

Abstract and Sealed

abstract class Shape { public abstract func Area() -> float; } class Square : Shape { public var side: float = 1.0; public override func Area() -> float => side * side; }

An abstract class cannot be instantiated; abstract members have no method body, and derived classes must override them. A sealed class forbids further inheritance.

Static Members

class Counter { public static var total: int = 0; public var id: int = 0; init() { Counter.total += 1; id = Counter.total; } } val c1 = new Counter(); val c2 = new Counter(); print(c1.id, c2.id, Counter.total);
1 2 2

Static members hang off the type and are accessed as TypeName.member. A static class is the dedicated form for static utility classes: members are forced static, the class is implicitly sealed and cannot be inherited. Initializers of static fields join global initialization.

Custom Indexers

operator [] declares index reads and operator []= declares index writes; the index can take any number of arguments (the matrix m[i, j] case):

class Box3 { val cells: list<int> = new list<int>(); init() { for i in 0..3 { cells.Add(0); } } public operator [](self, i: int) -> int => cells[i]; public operator []=(self, i: int, v: int) { cells[i] = v; } } func main() -> int { val b = new Box3(); b[1] = 9; b[1] += 5; print(b[1], b[0]); return 0; }
14 0
  • Compound assignments like b[i] += 5 desugar into a read plus a write; both go through the declared indexer.
  • Multi-argument indexing works the same way: operator [](self, x: int, y: int) -> int pairs with g[1, 2].
  • Indexer parameters do not support ref / out.

Notes

  • The bodyless semicolon form class Marker; is equivalent to an empty class body.
  • Marking init / deinit as virtual, override, or abstract raises MS4025: constructors and destructors take no part in virtual dispatch.
  • In the scripting edition of MushScript, the destructor deinit() fires only when a using block exits; ordinary block exits make no promise.
  • The primary constructor promotes its parameters to private val fields and joins the overload set alongside explicit inits; see “Primary Constructors and the Overload Set” above.
  • A class implements a protocol with impl, a clause separate from the inheritance :; see Protocol.
Last updated on October 11, 2026