Interfaces and generics
Interfaces
Section titled “Interfaces”An interface lists methods only — no fields, no properties:
interface IDisposable{ void Dispose();}
interface IShape{ float Area();}
struct Circle : IShape{ float Radius;
float Area() { return Math.PI * Radius * Radius; }}A struct can implement several interfaces (in the base list, after its base struct if it has one); the compiler checks that every method is actually implemented, with a matching signature.
Interface parameters (dynamic dispatch without allocation)
Section titled “Interface parameters (dynamic dispatch without allocation)”An interface can be the type of a ref or const ref parameter. The function then takes any struct that implements
the interface, and calls its methods through a method table — nothing is allocated:
string Describe(const ref IShape shape){ return $"{shape.Name()}: {shape.Area()}";}
void Enlarge(ref IShape shape, double factor){ shape.Grow(factor);}
var c = Circle { R = 1.0 };Console.WriteLine(Describe(c)); // const ref: the callee works on a copy on the caller's stackEnlarge(ref c, 2.0); // ref: the callee changes c itselfconst ref: the caller passes a copy of its struct (on its stack, released after the call), so methods that change the struct cannot change the caller’s value. Any value can be passed, alsoDescribe(Circle { R = 3.0 }).ref: the caller passes its variable withref, and the callee’s changes are visible.shape is Rect rtells which struct it is (and copies it out); an interface parameter can be passed on to another function with an interface parameter.- An interface is not a value type: no interface variables, fields, results, list elements, type arguments or lambda captures. That is what makes it free — the parameter can never outlive the struct it points to, so it needs no allocation and no reference count. For collections of different structs, use a sum type (inline, no allocation) or one list per struct type.
- Generics with a constraint (below) stay the zero-cost option when the struct type is known at compile time: calls are direct and can be inlined.
Sum types
Section titled “Sum types”A union holds one value of several member types, stored inline together with a tag — no allocation. It is the way
to keep different structs in one variable, field or list:
union Shape : IShape { Circle, Rect }
Shape s = Circle { R = 1.0 }; // a member converts to the unionvar shapes = List<Shape>.Create(); // different shapes in one list, stored in placeshapes.Add(s);shapes.Add(Rect { W = 2.0, H = 3.0 });
foreach (var shape in shapes) Console.WriteLine(shape.Area()); // IShape's methods are dispatched on the tag
if (s is Circle c) ... // the member again (a copy)
switch (s){ case Circle c: ... break; case Rect r: ... break;}- A
switchover a union withoutdefault:must handle every member (compile error otherwise), so adding a member shows every switch that has to handle it. - The members can be any value types (
union Token { int, string, bool }), each once. A union’s size is the size of its largest member plus the tag. - A union that lists interfaces (
: IShape) requires every member to implement them, and can call their methods directly; a method that changes the member changes it inside the union. It also satisfies constraints on those interfaces (where T : IShape) and can be passed toref/const ref IShapeparameters. - The default value of a union is empty:
isnever matches, and calling a method panics. - A union can be passed to a
threadfunction if all its members are thread-safe.
Generic structs and functions
Section titled “Generic structs and functions”struct Pair<T>{ T First; T Second;}
T Max<T>(T a, T b){ return a > b ? a : b;}
var p = Pair<int> { First = 1, Second = 2 };int m = Max(3, 7); // type argument inferredint m2 = Max<int>(3, 7); // or written out explicitlyThe compiler monomorphizes: a native specialization is generated for every type combination that’s actually used
(Max<int>, Max<double>, …). There’s no generic mechanism at run time and no boxing.
Constraints
Section titled “Constraints”T Max<T>(T a, T b) where T : IComparable<T>{ return a.CompareTo(b) > 0 ? a : b;}The compiler checks, at compile time, that every type used for T actually implements the required interfaces. In the
body, a value of type T offers the methods of its constraints (here CompareTo) and ToString(); calling another
method on it is an error, also if nothing calls Max yet. Operators (==, <, +, …) on T are checked for every
type the function is used with.
Numbers and string already implement IComparable<T>/IEquatable<T>/IHashable out of the box; your own structs
implement them by defining the matching methods (int CompareTo(T other), bool Equals(T other),
int GetHashCode()).
struct Money : IComparable<Money>{ int64 Cents;
int CompareTo(Money other) { return Cents < other.Cents ? -1 : (Cents > other.Cents ? 1 : 0); }}This is exactly what Dictionary<TKey, TValue> needs from its key type (IEquatable + IHashable) and what
List<T>.Sort() needs from its element type (IComparable<T>) — see
arrays, strings and collections.
Next: Enums.