Classes, interfaces and traits

Everything a class carries: a base class, virtual dispatch, abstract members, interfaces (with default bodies and static abstract members), traits in PHP's model, properties and operator overloading — plus the two rules that hold the whole page together, the implicit receiver and the free order of declaration.

The shape of a declaration, its fields and its modifiers are types.md.


Inheritance #

A class names one base class, first in the : list, and any number of interfaces after it. The base's fields are laid out first, so an offset the base publishes keeps its value in every class below it.

class Name : Base, IfaceA, IfaceB { members }

A cycle in the chain is refused, and so is a struct as a base.

The receiver is not written #

A method declares no receiver. Inside the body, a bare name is a member of this when no local or parameter carries the name — C#'s own rule, local shadows field. this.side says the same thing out loud, this is only valid inside the body of a type, and neither this nor base exists in a static member.

base.m() calls the base class's own implementation directly, without the vtable, which is what lets an override build on the method it overrides instead of recursing.

virtual and override #

A virtual method takes a slot in the vtable; an override fills the slot its base declared. Slots are keyed by (name, signature), so two overloads of one name are two slots and each override lands on its own. A method that hides an inherited virtual without saying override is refused, and so is an override of a method no base declares. A struct has no vtable at all, so neither word is accepted there.

// expect-exit: 42
#include "rt.tk"

class Shape {
    public i64 side;

    public i64 twice() {
        return side * 2;                         // `side` is `this.side`
    }

    public virtual i64 area() {
        return this.side;
    }
}

class Square : Shape {
    public override i64 area() {
        return base.area() * side;               // the base's own body, directly
    }
}

i64 area_of(Shape s) {
    return s.area();                             // through the vtable
}

i64 main() {
    Shape p = new Shape;
    p.side = 3;
    if (p.area() != 3) return 1;
    if (p.twice() != 6) return 2;

    Square q = new Square;
    q.side = 5;
    if (q.area() != 25) return 3;

    Shape r = new Square;                        // a derived object in a base-typed slot
    r.side = 4;
    if (r.area() != 16) return 4;                // the override answers
    if (area_of(r) != 16) return 5;

    return p.area() + q.area() + r.area() - 2;   // 3 + 25 + 16 - 2
}

Constructors and destructors #

Name(params) { } inside class Name is a constructor: overloadable by signature, chosen by the argument count at new Name(args), and written with no return type. : base(args) chains to the base's own, and a base that declares only constructors taking arguments must be named that way.

~Name() { } is the destructor: no modifier, no parameter, one per class. It runs when the reference count reaches zero, before the fields are released, and a chain runs the derived class's first and the base's last — the reverse of the constructor chain.

new Name with no matching constructor still hands out the zeroed object.

// expect-exit: 42
#include "rt.tk"

i64 order = 0;

class Base {
    public i64 tag;

    public Base(i64 t) {
        tag = t;
    }

    ~Base() {
        order = order * 10 + 1;
    }
}

class Derived : Base {
    public Derived(i64 t) : base(t + 1) {
    }

    ~Derived() {
        order = order * 10 + 2;
    }
}

i64 main() {
    {
        Derived d = new Derived(1);
        if (d.tag != 2) return 1;                // `: base(t + 1)` ran first
    }
    if (order != 21) return 2;                   // the derived's destructor, then the base's
    if (rt_live() != 0) return 3;
    return 42;
}

abstract #

An abstract class has no object of its own: new on it is refused, and it emits no constructor. An abstract member has no body and takes an empty vtable slot; the first class down the chain that is not abstract has to fill every one of them with an override, which is the conformance check an interface already gets. An abstract class in between may leave what it inherited open and add members of its own. A property is abstract by the same rule, one slot per accessor and no backing field. An abstract member outside an abstract class is refused, and so is abstract on a static member.

// expect-exit: 42
#include "rt.tk"

abstract class Shape {
    public i64 side;

    public abstract i64 area();

    public abstract i64 Scale { get; }

    public i64 twice() {
        return area() * 2;                       // a concrete method calling the abstract one
    }
}

abstract class Sided : Shape {
    public i64 doubled() {
        return side * 2;                         // adds its own, leaves `area` open
    }
}

class Square : Sided {
    public override i64 area() {
        return side * side;
    }

    public override i64 Scale {
        get => side + 1;
    }
}

i64 main() {
    Square q = new Square;
    q.side = 4;

    Shape s = q;
    if (s.area() != 16) return 1;
    if (s.twice() != 32) return 2;
    if (s.Scale != 5) return 3;

    Sided d = q;
    if (d.doubled() != 8) return 4;
    return 42;
}

Interfaces #

An interface declares methods and properties — never a field — and produces no object. A class that names one in its : list carries an interface table, and a call through an interface-typed value walks that table, so two unrelated classes answer the same interface and a class implements it at any vtable slot.

Every interface member is public: private/protected on one is refused, and a method that implements an interface has to be public.

An interface may:

writtennotes
declare a methodi64 area();no receiver, as in a class
carry a default bodyi64 doubled() { return area() * 2; }C# 8; the class that does not redeclare it points at this symbol
declare a propertyi64 Label { get; set; }accessors only, no bodies
declare a static abstract memberstatic abstract i64 unit();C# 11; the type provides it, resolved at compile time, no slot
extend another interfaceinterface I2 : I1 { }the conformance set is flattened: a class naming I2 owes I1 too

Inside a default body, this is the implementing receiver typed as the interface, and every member reached from it dispatches through the table — so the class that redeclared a member is the one that answers, including for the default that called it.

// expect-exit: 42
#include "rt.tk"

interface Shape {
    i64 side();

    i64 area() {
        return this.side() * this.side();        // a default body
    }

    i64 doubled() {
        return area() * 2;                       // unqualified: through the table
    }

    static abstract i64 unit();

    i64 Label { get; set; }
}

class Square : Shape {
    public i64 w;
    public i64 Label { get; set; }

    public i64 side() {
        return w;
    }

    public static i64 unit() {
        return 5;
    }
}

class Fixed : Shape {
    public i64 Label { get; set; }

    public i64 side() {
        return 3;
    }

    public i64 area() {
        return 100;                              // redeclared: the default defers to it
    }

    public static i64 unit() {
        return 7;
    }
}

i64 doubled_of(Shape s) {
    return s.doubled();
}

i64 main() {
    Square q = new Square;
    q.w = 4;
    Shape s = q;
    if (s.area() != 16) return 1;
    if (doubled_of(s) != 32) return 2;

    Fixed f = new Fixed;
    Shape t = f;
    if (t.area() != 100) return 3;
    if (t.doubled() != 200) return 4;            // the default reached the redeclaration

    s.Label = 11;
    if (q.Label != 11) return 5;                 // a property, through the table

    if (Square.unit() != 5) return 6;            // static abstract, answered by the type
    if (Fixed.unit() != 7) return 7;

    return t.area() - s.area() - Square.unit() - Fixed.unit() - 30;   // 100-16-5-7-30
}

Interface inheritance and the diamond #

interface IC : IA, IB where both declare m() is C#'s diamond: the class implements m once and the tables of IA, IB and IC all point at that one symbol. There is no covariance or contravariance.

// expect-exit: 42
#include "rt.tk"

interface IA {
    i64 m();
}

interface IB {
    i64 m();
}

interface IC : IA, IB {
}

class Box : IC {
    public i64 v;

    public i64 m() {
        return v * 2;
    }
}

i64 main() {
    Box bx = new Box;
    bx.v = 3;
    IA a = bx;
    IB b = bx;
    IC c = bx;
    if (a.m() != 6) return 1;
    if (b.m() != 6) return 2;
    if (c.m() != 6) return 3;                    // IC declares nothing of its own
    return a.m() + b.m() + c.m() + 24;
}

Assignment compatibility #

A derived class goes into a base-typed slot, and a class goes into a slot typed by an interface it implements — those two conversions, and nothing else. Both are the same reference; no object is built to perform them.


Traits #

A trait is PHP's, not an interface: its members are copied into the class at compile time, so a trait field lands in the class's own layout and a trait method is the class's own method. A class brings one in with use Name;, and a trait may itself use another.

The three precedences are PHP's:

wins
the class's own member vs the trait'sthe class's
the trait's member vs one inherited from the basethe trait's, and it takes the base's virtual slot
a trait used by a traitflattened into whoever uses the outer one

A trait method names a field with no receiver, and it resolves against the class the member was copied into. Only a class uses a trait; a trait declares no const, insteadof/as in a use block are not taught, and a trait is never a base class nor an interface.

// expect-exit: 42
#include "rt.tk"

trait Counted {
    protected i64 n;

    public i64 bump(i64 k) {
        n = n + k;                               // the field of whoever uses it
        return n;
    }

    public i64 kind() {
        return 7;
    }

    public i64 label() {
        return 8;
    }
}

trait Tagged {
    use Counted;

    public i64 tag() {
        return 10;
    }
}

class Base {
    public virtual i64 kind() {
        return 99;
    }

    public virtual i64 label() {
        return 98;
    }
}

class Widget : Base {
    use Tagged;

    public i64 w;

    public override i64 label() {
        return 5;
    }
}

i64 main() {
    if (WIDGET_W != 16) return 1;                // the class's own field
    if (WIDGET_N != 24) return 2;                // the trait's, in the same layout

    Widget g = new Widget;
    if (g.bump(3) != 3) return 3;
    if (g.tag() != 10) return 4;                 // the outer trait's own method
    if (g.kind() != 7) return 5;                 // the trait beats the base
    if (g.label() != 5) return 6;                // the class beats the trait

    Base b = g;
    if (b.kind() != 7) return 7;                 // ...both through the base's slot
    return g.bump(0) + g.tag() + g.kind() + g.label() + 17;   // 3+10+7+5+17
}

Properties #

A property reads like a field at every use site and is a pair of methods everywhere else. The three C# forms are here:

formwritten
autopublic i64 Raw { get; set; } — a private backing field is generated
expression-bodiedpublic i64 Area { get => side; set => side = value; }
block-bodiedpublic i64 Twice { get { return Area * 2; } }

value is the name the set is handed, and it means that inside a set and nowhere else. An accessor may narrow the property's visibility ({ get; private set; }) but never widen it. A property is virtual/override/abstract/static like a method, with one vtable slot per accessor — so an override that redeclares only the get inherits the set. Writing to a get-only property, declaring a property with no accessor, declaring the same accessor twice and calling a property with () are each refused.

// expect-exit: 42
#include "rt.tk"

class Shape {
    public i64 side;

    public virtual i64 Area {
        get => side;
        set => side = value;
    }

    public i64 Twice {
        get { return Area * 2; }
    }
}

class Square : Shape {
    public override i64 Area {
        get => side * side;                      // only the `get`; the `set` is inherited
    }
}

class Counter {
    public i64 N { get; private set; }

    public i64 bump(i64 k) {
        N = N + k;                               // the private setter, from inside
        return N;
    }
}

i64 main() {
    Shape s = new Shape;
    s.side = 3;
    if (s.Area != 3) return 1;
    s.Area = 5;
    if (s.side != 5) return 2;
    if (s.Twice != 10) return 3;

    Square q = new Square;
    q.side = 4;
    Shape b = q;
    if (b.Area != 16) return 4;                  // the override, through the slot
    b.Area = 6;                                  // the setter it did not redeclare
    if (q.side != 6) return 5;

    Counter c = new Counter;
    if (c.bump(3) != 3) return 6;
    if (c.N != 3) return 7;

    return b.Area + c.N + 3;                     // 36 + 3 + 3
}

Operators #

An operator is a public static member that names both operands: no receiver, no this, no vtable slot. Which declaration a site lands on is decided by the static types of the operands and by nothing else.

public static T operator<op>(A a, B b)      // binary
public static T operator<op>(A a)           // unary
binary tokens+ - * / % == != < <= > >= & | ^ << >>
unary tokens- ! ~ +
candidates of a sitethe operators declared by the type of either operand, and by its bases
requiredat least one parameter of the declaring type; a return value; no default on a parameter
pairs==/!=, </>, <=/>= are declared together
refusedvirtual/override/abstract, this, an operator in an interface, a token that is not overloadable

Resolution runs exact first, then over integer literals, then over base classes — and among bases the closest ancestor wins rather than the site being called ambiguous. A reversed form (2 + v, an i64 on the left) is an ordinary declaration, because both operands are parameters. The core's own arithmetic is untouched: 20 + 22 is still the core's sum.

// expect-exit: 42
#include "rt.tk"

class Vec {
    public i64 x;
    public i64 y;

    public static Vec operator+(Vec a, Vec b) {
        Vec r = new Vec;
        r.x = a.x + b.x;
        r.y = a.y + b.y;
        return r;
    }

    public static Vec operator+(i64 k, Vec v) {  // the reversed form
        Vec r = new Vec;
        r.x = k + v.x;
        r.y = k + v.y;
        return r;
    }

    public static Vec operator-(Vec a) {         // the unary form
        Vec r = new Vec;
        r.x = 0 - a.x;
        r.y = 0 - a.y;
        return r;
    }

    public static i64 operator==(Vec a, Vec b) {
        if (a.x != b.x) return 0;
        if (a.y != b.y) return 0;
        return 1;
    }

    public static i64 operator!=(Vec a, Vec b) {
        return !(a == b);
    }
}

Vec combine(Vec a, Vec b) {
    return a + b;                                // both operands are parameters
}

i64 main() {
    Vec a = new Vec;
    a.x = 10;
    a.y = 4;
    Vec b = new Vec;
    b.x = 20;
    b.y = 8;
    Vec c = new Vec;
    c.x = 30;
    c.y = 12;

    if (((a + b) == c) != 1) return 1;           // `+` resolved first, then `==`
    if (((a + b) != c) != 0) return 2;
    if ((combine(a, b) == c) != 1) return 3;

    Vec d = -a;
    if (d.x != 0 - 10) return 4;

    Vec e = 2 + a;
    if (e.x != 12) return 5;

    Vec s = a + b;
    return s.x + s.y;                            // 30 + 12
}

partial #

The same class may be declared in more than one place and is united at compile time. The first part opens the row, every part after it adds members, and the type closes at its first use — a part written after that is refused where it stands. The base class is named in the first part, before any member is laid out; interfaces are a union and may come in any part. A generic is partial by the same rule. There is no partial method.

// expect-exit: 42
#include "rt.tk"

partial class Shape {
    public i64 w;

    public i64 area() {
        return w * h;                            // `h` is declared in the other part
    }
}

class Between {
    public i64 z;                                // declared between the parts, unaffected
}

partial class Shape {
    public i64 h;
}

i64 main() {
    Shape s = new Shape;
    s.w = 6;
    s.h = 7;
    Between t = new Between;
    t.z = 0;
    return s.area() + t.z;
}

Order of declaration is free #

A type may be used above the line that declares it: as a field, a parameter, a return type, a local, a base class, an interface, the target of new, the owner of a static member or a const, and as the type a use names. The declaration is materialized on the spot, so the base is whole — fields, virtual slots and constructor — before the derived class lays itself out.

// expect-exit: 42
#include "rt.tk"

class Holder {
    public Circle mine;                          // `Circle` is declared below

    public Circle same(Circle c) {
        return c;
    }
}

class GrandDog : Dog {
    public override i64 speak() {
        return base.speak() + 10;
    }
}

class Dog : Animal {
    public Dog(i64 n) : base(n) {
    }

    public override i64 speak() {
        return base.speak() + 1;
    }
}

class Animal {
    public i64 legs;

    public Animal(i64 n) {
        legs = n;
    }

    public virtual i64 speak() {
        return legs;
    }
}

class Circle {
    public i64 r;
}

i64 main() {
    Holder h = new Holder;
    Circle c = new Circle;
    c.r = 7;
    h.mine = c;
    if (h.same(c).r != 7) return 1;

    Dog d = new Dog(4);
    if (d.speak() != 5) return 2;

    GrandDog g = new GrandDog;                   // no constructor of its own
    if (g.speak() != 11) return 3;               // 0 + Dog's +1 + GrandDog's +10

    return h.mine.r + d.speak() + g.speak() + 19;   // 7 + 5 + 11 + 19
}

Limits #

limitvalue
base classes per class1
destructors per class1
parameters of a method12, the receiver and the vtable pointer of a virtual call included
methods, summed across all types128
virtual slots, summed across all types128
interface methods, summed across all interfaces128
interfaces named in one class's : list8
(class, interface) pairs, summed across all classes64
constructors, summed across all classes32
traits declared in one source32
properties, summed across all types64
operators, summed across all types64
a type inside a typenot taught
a partial methodnot taught
insteadof/as in a use blocknot taught

The full catalogue of refusals is not-yet.md and diagnostics.md.

Edit this page