Constraints in Generics
Go Up to Generics Index
Constraints can be associated with a type parameter of a generic. Constraints declare items that must be supported by any particular type passed to that parameter in a construction of the generic type.
Contents
Specifying Generics with Constraints
Constraint items include:
- Zero, one, or multiple interface types
- Zero or one class type
- The reserved word "constructor", "class", or "record"
You can specify both "constructor" and "class" for a constraint. However, "record" cannot be combined with other reserved words. Multiple constraints act as an additive union ("AND" logic).
The examples given here show only class types, although constraints apply to all forms of generics.
Declaring Constraints
Constraints are declared in a fashion that resembles type declarations in regular parameter lists:
type
TFoo<T: ISerializable> = class
FField: T;
end;
In the example declaration given here, the 'T' type parameter indicates that it must support the ISerializable interface. In a type construction like TFoo<TMyClass>, the compiler checks at compile time to ensure that TMyClass actually implements ISerializable.
Multiple Type Parameters
When you specify constraints, you separate multiple type parameters by semicolons, as you do with a parameter list declaration:
type
TFoo<T: ISerializable; V: IComparable>
Like parameter declarations, multiple type parameters can be grouped together in a comma list to bind to the same constraints:
type
TFoo<S, U: ISerializable> ...
In the example above, S and U are both bound to the ISerializable constraint.
Multiple Constraints
Multiple constraints can be applied to a single type parameters as a comma list following the colon:
type
TFoo<T: ISerializable, ICloneable; V: IComparable> ...
Constrained type parameters can be mixed with "free" type parameters. For example, all the following are valid:
type
TFoo<T; C: IComparable> ...
TBar<T, V> ...
TTest<S: ISerializable; V> ...
// T and V are free, but C and S are constrained
Types of Constraints
Interface Type Constraints
A type parameter constraint may contain zero, one, or a comma separated list of multiple interface types.
A type parameter constrained by an interface type means that the compiler will verify at compile time that a concrete type passed as an argument to a type construction implements the specified interface type(s).
For example:
type
TFoo<T: ICloneable> ...
TTest1 = class(TObject, ICloneable)
...
end;
TError = class
end;
var
X: TFoo<TTest1>; // TTest1 is checked for ICloneable support here
// at compile time
Y: TFoo<TError>; // exp: syntax error here - TError does not support
// ICloneable
Class Type Constraints
A type parameter may be constrained by zero or one class type. As with interface type constraints, this declaration means that the compiler will require any concrete type passed as an argument to the constrained type param to be assignment compatible with the constraint class.
Compatibility of class types follows the normal rules of OOP type compatibilty - descendent types can be passed where their ancestor types are required.
Constructor Constraints
A type parameter may be constrained by zero or one instance of the reserved word "constructor". This means that the actual argument type must be a class that defines a default constructor (a public parameterless constructor), so that methods within the generic type may construct instances of the argument type using the default constructor of the argument type, without knowing anything about the argument type itself (no minimum base type requirements).
In a constraint declaration, you can mix "constructor" in any order with interface or class type constraints.
Class Constraint
A type parameter may be constrained by zero or one instance of the reserved word "class". This means that the actual type must be a class type.
Record Constraint
A type parameter may be constrained by zero or one instance of the reserved word "record". This means that the actual type must be a value type (not a reference type). A "record" constraint cannot be combined with a "class" or "constructor" constraint.
Type Inferencing
When using a field or variable of a constrained type parameter, it is not necessary in many cases to typecast in order to treat the field or variable as one of the constrained types. The compiler can infer which type you're referring to by looking at the method name and by performing a variation of overload resolution over the union of the methods sharing the same name across all the constraints on that type.
For example:
type
TFoo<T: ISerializable, ICloneable> = class
FData: T;
procedure Test;
end;
procedure TFoo<T>.Test;
begin
FData.Clone;
end;
The compiler looks for "Clone" methods in ISerializable and ICloneable, since FData is of type T, which is guaranteed to support both those interfaces. If both interfaces implement "Clone" with the same parameter list, the compiler issues an ambiguous method call error and require you to typecast to one or the other interface to disambiguate the context.