Generic types are best understood by comparing them to regular, non-generic types and their limitations. For example, one could implement a "StringList" as a regular class that can contain a list of Strings. One could make that list as fancy as one likes, but it would always be limited to Strings and only Strings. To have a list of, say, Integers, the entire list logic would need to be implemented a second time.
Alternatively, one could implement an "ObjectList" that could hold any
Object. Since Strings and Integers can both be objects, both could be stored in this list. But now we're sacrificing type safety. Any given ObjectList instance might contain Strings, Integers, or indeed any other kind of object. At each access, one would need to cast, and type check.
By contrast, a generic "
List<T>" class can be written that holds an as-of-yet undefined type of object. The entire class can (and, indeed, must) be implemented without ever knowing what type of objects will be in the list, only referring to them as
T. But: When using the generic class for a concrete purpose, it can be instantiated with a concrete type replacing
T: as a
List<String> or a
List<Integer> for example.
type List<T> = public Class end; List<Key,Value> = public class end;
The type parameters can be arbitrary identifiers. It is common to use short single-letter uppercase names such as
V, to make the type parameters stand out in the remainder of the code, but that is mere convention. Any valid identifier is allowed.
Once declared as part of the generic type's name, these type parameters become valid types that can be used throughout the declaration and implementation, as if they were regular, well-known type names. For example they can be used as type for Method Parameters or Results, or as variables inside a method body.
type List<T> = public class public method Add(aNewItem: T); property Items[aIndex: Integer]: T; end;
Of course, since little is know about what
T is, there are limitations to what the generic class can do with instances of
T in code. While some lists will contain Strings, others might contain Integers – so it would not be safe to, for example, call a string-specific method on
This is where constraints come in.
If a generic type needs more specific control over what subset of types are allowed for its generic parameters, it can declare one or more constraints, using the
Essentially, a constraint limits the generic class from being instantiated with a concrete type that does not fulfill the conditions.
There are four types of supported constraints:
is class— requires the concrete type to be a Class (i.e. disallows records or value types).
is record— requires the concrete type to be a Record or (i.e. disallows classes).
TypeName— requires the concrete type to implement the specified Interface or descend from the specified Class.
has constructor— requires the concrete type to have a parameter-less constructor.
Of course individual constraints can be combined. For example constraining the above list in two ways could give it additional capabilities:
type List<T> = public class where T is IComparable, T has constructor; public method New: T; begin result := new T; // made possible because of `where T has constructor` Add(result); end; method Sort(); begin ... complex sorting code if self[a].CompareTo(self[b]) then // made possible by `where T is IComparable` Switch(a,b); ... more complex sorting code end; end;
where T has constructor constraint allows the new list code to create new instances of whatever type
T is, at runtime. And the
where T is IComparable constraint allows it to call members of that interface on
T (without cast, because
T is now assured to implement
Of course on the downside, the
List<T> class is now more restricted and can no longer be used with types that do not adhere to these constraints.
Adding constraints is a fine balance between giving a generic class more flexibility, on the one hand, and limiting its scope on the other. One possible solution for this is to declare additional constraints on an Extension, instead:
Constraints on Extensions
When declaring an Extension for a generic class, it is allowed to provide additional constraints that will applicable only on the extension members.
This keeps the original class free from being constrained, but limits the extension members to be available to those instances of the class that meet the constraints. For example, one could make the
List<T> from above more useful for strings:
List<T> = public class where T is String; public method JoinedString(aSeparator: String): String; begin var sb := new StringBuilder(); for each s in self index i do begin if i > 0 then sb.Append(aSeparator); sb.Append(s); // we know s is a String, now end; end; end;
var x := List<String>; var xy:= List<Button>; x.JoinedString(); y.JoinedString(); // compiler error, no such member.
In this example, the new
JoinedString method would only be available on
List<String>, as a list with any other type would not satisfy the constraint.
Co- and Contra-Variance
A generic Interface can be marked as either co- or contra-variant on a type parameter, by prefixing it with the
in keyword, respectively:
IReadOnlyList<out T> = public interface GetItemAt(aIndex: Integer): T; end; IWriteOnlyList<in T> = public interface SetItemAt(aIndex: Integer; aItem: T); end;
A co-variant generic parameter (marked with
out) makes a concrete type compatible with base types of the type parameter. For example, a
IReadOnlyList<Button> can be assigned to a
This makes sense, because any
Button is also an
Object. Since the
IReadOnlyList only uses the type
T outgoing, as method results (or
out Parameters), any call to a list of
Buttons can be assure to return an
The reverse would not be case, if the original
List class were co-variant, on could add arbitrary Objects to a list of Buttons – and that would be bad.
By contrast, a contra-variant generic parameter (marked with
in) makes a concrete type compatible with descendant types of the type parameter. For example, a
IWriteOnlyList<Object> can be assigned to a
Once again, this makes sense, because
IWriteOnlyList only uses the type
T incoming, as method parameter. Because a
IWriteOnlyList<Object> can hold any object, it is perfectly safe to be treated as a
IWriteOnlyList<Button> – the only thing that can ever happen through this interface is that buttons get added to the list – and buttons are objects.
And again, the reverse would not be case. If the original
List class were contra-variant, one could retrieve arbitrary Objects from a List if Objects, from code that expects to get buttons.
Co- and Contra-Variance is allowed only on Interface types. Generic Classes or Records cannot be marked as variant.
Co- and Contra-Variance is supported on the .NET platform only.