Summary#
A class derives from another with :, exactly as in C#. It inherits the base's fields and methods:
class Shape {
public string Name;
public string Describe() {
return Name;
}
}
class Circle : Shape {
public decimal Radius;
}
string DescribeACircle() {
var c = new Circle { Name = "small", Radius = 2m };
return c.Describe(); // the method Shape declares, called on a Circle
}Signature#
class Derived : Base { … } // inherits Base's fields and methods
sealed class Leaf { … } // no type may derive from LeafDescription#
What does a subclass inherit?#
Fields and methods, to any depth. A three-level chain works the way it reads, and a member declared anywhere above is available below:
class Shape {
public string Name;
}
class Round : Shape {
public decimal Radius;
}
class Dot : Round { }
string NameOfADot() {
var d = new Dot { Name = "tiny", Radius = 0m };
return d.Name; // declared two levels up
}A derived value fits a base slot#
This is the point of a hierarchy: code written against Shape accepts every shape. The conversion is implicit and
needs nothing written:
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }
string NameThrough() {
Shape s = new Circle { Name = "c", Radius = 2m };
return s.Name;
}It is one-way, as in C#. A Shape is not assignable to a Circle, because not every shape is one — going the
other way needs a type test.
The same rule applies wherever two values meet and one type has to describe both — a conditional, a switch
expression, a ?? fallback. The answer is the base of the two:
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }
string Pick(bool round, int kind) {
var c = new Circle { Name = "circle", Radius = 2m };
var p = new Shape { Name = "plain" };
Shape a = round ? c : p; // the branches unify to Shape
Shape b = kind switch { 1 => c, _ => p }; // so do the arms
Shape d = a ?? c; // and so do the sides of `??`
return a.Name + b.Name + d.Name;
}Because the result is a Shape, only Shape's members are readable through it — reach for a
type test to get back to Radius.
Two SIBLINGS take the type they are written into. A Circle and a Square are both Shapes, but neither is the
other, so there is no type to infer — and where the type is written, that is the answer:
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }
class Square : Shape { public decimal Side; }
class Plot { public Shape Primary; }
string Show(Shape s) { return s.Name; }
string Pick(bool round) {
var c = new Circle { Name = "circle", Radius = 1m };
var q = new Square { Name = "square", Side = 2m };
Shape s = round ? c : q; // a declared local
var p = new Plot { Primary = round ? c : q }; // a member
return Show(round ? c : q) + s.Name + p.Primary.Name; // an argument
}It works wherever the target is written: a declared local, a parameter, a member, an assignment, a return, and the
arms of a switch expression. What it does not do is invent one — var s = round ? c : q; writes the value
into nothing, so there is nothing to take, and the compiler says so and names the fix. (Same in C#.)
Can a Circle[] be used as a Shape[]?#
A Circle[] is a Shape[], and needs nothing written — as in C#. This holds in every position: a local, a
parameter, a return, a class member.
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }
int CountShapes(Shape[] shapes) { return shapes.Count(); }
int HowMany() {
Circle[] cs = [new Circle { Name = "a", Radius = 1m }, new Circle { Name = "b", Radius = 2m }];
Shape[] xs = cs;
return CountShapes(xs);
}A List<T> is different, and deliberately so: it can be appended to, so a List<Circle> is not a
List<Shape>. Handing one over would let the receiver add a plain Shape to your list of circles. If the receiver
only reads, declare it Shape[] — a List passes straight into a read-only sequence.
An array of a type that HAS subtypes cannot be written through. This is the other half of the rule above, and
the reason the conversion is safe: since a Shape[] may really be a Circle[], storing a plain Shape into one
would leave an element missing the members the narrower array promises.
Shape[] xs = circles; // fine — read it all you like
xs[0] = new Shape { Name = "x" }; // refused: `Shape` has subtypes, so this array may be a `Circle[]`C# allows that write and throws ArrayStoreException when it runs; here it is the same rule, moved to where you can
see it. An array of a type nothing derives from — including every scalar array, int[], double[], string[] — is
written exactly as in C#. When you need to write into a polymorphic sequence, use a List<Shape>: it is invariant,
which is what makes it safe to write.
sealed closes the class#
sealed says no type may derive. Write it when a class is meant to be the end of its line:
sealed class Candle : Mark { } // deriving from Candle is a compile errorSpecialising a method: virtual and override#
A base method marked virtual may be replaced by a derived one marked override. The call runs the method of the
value's actual type, whatever type the slot holding it is declared as:
class Shape {
public string Name;
public virtual decimal Area() { return 0m; }
}
class Circle : Shape {
public decimal Radius;
public override decimal Area() { return Radius * Radius * 3m; }
}
decimal AreaThroughTheBase() {
Shape s = new Circle { Name = "c", Radius = 2m };
return s.Area(); // 12 — Circle's body, not Shape's
}Both words are required, each for a different mistake. Without virtual on the base, adding a method to a base
class would silently change what every subclass sharing that name does. Without override on the derived one, an
accidental name collision would read as a deliberate specialisation. Redeclaring a method that is not virtual is
refused, and the refusal names the word that is missing.
base.M() calls the version the derived class inherits — what makes an override able to EXTEND the base rather
than replace it:
class Shape {
public string Name;
public virtual decimal Area() { return 2m; }
}
class Circle : Shape {
public decimal Radius;
public override decimal Area() { return base.Area() + 1m; } // 3 — Shape's answer, plus one
}
decimal AreaOfACircle() {
Shape s = new Circle { Name = "c", Radius = 1m };
return s.Area();
}base looks up the chain, not just one step: if the immediate parent declares nothing by that name, the call runs
the nearest ancestor that does. It is only meaningful inside a class member's body, and a local variable named
base shadows it.
Constructing the base: : base(…)#
A derived class's constructor runs the base class's constructor first, and says which one with : base(…):
class Shape {
public string Name;
public decimal Width;
public Shape(string n, decimal w) { Name = n; Width = w; }
}
class Circle : Shape {
public decimal Radius;
public Circle(string n, decimal r) : base(n, r * 2m) { Radius = r; }
}
string BuildOne() {
var c = new Circle("small", 2m);
return c.Name; // "small" — set by Shape's constructor
}The arguments are an ordinary argument list: as many as the base constructor takes, in any expression, and by name
(: base(n, w: 4m)) if you prefer. : base() is how you call a parameterless base constructor explicitly.
A constructor with no initializer runs the base's parameterless constructor, exactly as in C#. So if the base declares a constructor that takes arguments, the derived one has to say what to pass — and the compiler asks for it by name. A base class with no constructor at all needs nothing: its fields take the defaults their declarations give.
Members the base constructor assigns count as assigned, so you do not have to supply them again at the create site.
: this(…) — chaining to another constructor of the same class — needs constructor overloading, which is not
available yet.
abstract — a shape to derive from#
An abstract class cannot be created; it exists for other types to fill in. An abstract method declares what a
subclass must provide and has no body:
abstract class Shape {
public string Name;
public abstract decimal Area(); // no body — every Shape has one, but Shape does not say what
}
class Circle : Shape {
public decimal Radius;
public override decimal Area() { return Radius * Radius * 3m; }
}
decimal AreaOfACircle() {
Shape s = new Circle { Name = "c", Radius = 2m };
return s.Area(); // 12 — Circle's body
}An abstract method is already the thing a subclass overrides, so it needs no virtual. The first concrete class
below it must provide a body — a class that does not is asked for one by name, unless it is abstract too, in which
case the obligation passes down. abstract and sealed are opposites and cannot both be written: one says the type
must be derived from, the other that it must not.
protected — visible down the chain, and nowhere else#
A class member is private unless it says otherwise, and private means this class only — a subclass cannot see
it. protected is the middle setting: reachable from the declaring class and from anything that derives from it.
class Shape {
protected string Tag; // subclasses may read and write it
private string secret; // this class only, even for a subclass
public Shape(string t) { Tag = t; secret = "hidden"; }
protected string Describe() { return "[" + Tag + "]"; } // methods take it too
}
class Circle : Shape {
public decimal Radius;
public Circle(string t, decimal r) : base(t) { Radius = r; }
public string Label() { return Tag + Describe(); } // both reachable here
}
string BuildLabel() {
var c = new Circle("c", 2m);
return c.Label(); // "c[c]"
}It reaches the whole chain, not one step: a class deriving from Circle sees Shape's protected members too.
From outside the hierarchy the member does not exist — c.Tag in a top-level function is a compile error naming the
fix, which is to derive from the class rather than to reach into it.
Can a class derive from an entity?#
A class may derive only from a class. Mixing the kinds is refused in both directions, because they are different things wearing one word: an entity hierarchy is rows in a table with a discriminator column, and a class is a value in memory with no table at all.
Errors#
| you wrote | what you get |
|---|---|
| a method already declared on the base | refused, naming virtual/override as what is missing |
class C : SomeSealedClass | refused — sealed means no type may derive |
class C : SomeEntity (or an entity deriving from a class) | refused — a class and an entity are different kinds |
Circle c = someShape; | refused — the upcast is one-way; test the type instead |
var s = f ? circle : square; | refused — two siblings have no common type and var supplies no target; declare the type, or cast one branch |
a List<Circle> where a List<Shape> is wanted | refused — a List can be appended to, so it is invariant; declare Shape[] if it is only read |
shapes[0] = new Shape { … } where Shape has subtypes | refused — the array may be a Circle[]; read it freely, or use a List<Shape> to write |
See also#
- Testing which class a value is — asking which type a value actually is, and narrowing to it
- class methods — behaviour with a receiver
- entity Sub : Base — the same word for rows, and why it works differently
- Classes — what a class is