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.
- 1 Specifying Generics with Constraints
- 2 Types of Constraints
- 3 Type Inferencing
- 4 See Also
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.
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 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).
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.
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 argument type's default constructor, without knowing anything about the argument type itself (no minimum base type requirements).
Note: classes passed as type parameters may define other constructors or even "hide" the default parameterless constructor. However, only the default parameterless constructor can be called from the methods of the generic type. Therefore, one must be certain that class types passed to generic types with the constructor will function completely and correctly when constructed with the default parameterless constructor alone.
If constructors with parameters must be called, an alternative is to use a class constraint that defines class methods which call the appropriate constructors as needed. Only use the constructor constraint when class instances can be constructed with the default constructor without parameters.
In a constraint declaration, you can mix "constructor" in any order with interface or class type constraints.
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.
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.
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.
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.