Constraints in Generics

From RAD Studio
Jump to: navigation, search

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.

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.

Interface Constraint

A type parameter may be constrained by zero or one instance of the reserved word "interface". This ensures the type implements an interface, allowing the generic code to call interface methods safely.

The interface constraint defers from specifying IInterface. When the IInterface is specified, or any other interface, the compiler allows the generic to initiate with that interface or a subtype, and also with a class that implements that interface. The interface constraint does not allow any class types.

For example, see the code below:

type
  IFoo = interface
    procedure Hello;
  end;
  TFoo = class(TInterfacedObject, IFoo)
    procedure Hello;
  end;
  TDerived = class(TFoo)
  end;
  TMyClass1<T:IFoo> = class
  end;
  TMyClass2<T:IFoo, interface> = class
  end;

var
  C1: TMyClass1<IFoo>; // ok
  C2: TMyClass2<IFoo>; // ok
  C3: TMyClass1<TFoo>; // ok
  C4: TMyClass2<TFoo>; // Error E2667 Type parameter '%s' must be an interface type
end.

Unmanaged Constraint

A type parameter may be constrained by zero or one instance of the reserved word "unmanaged". Restricts the generic type to simple types.

The unmanaged constraint can be used for generic types that are placed in the variant section of a record. The unmanaged constraint is the response when the record constraint no longer allows the ones with managed fields.

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 requires you to typecast to one or the other interface to disambiguate the context.

L-Value cast

It is possible to allow L-Value cast for the value type variable declared with a generic type. In a method of a generic class, it is possible to cast the result of a generic method returning the generic type itself, depending on the concrete type being used.

For example:

class function TMyData<T>.OperateSubtract(const L, R: T): T;
begin
  if (GetTypeKind(T) = tkInteger) and (SizeOf(T) = 1) then
    Byte(Result) := Byte(L) - Byte(R) // New cast!
  else if (GetTypeKind(T) = tkInteger) and (SizeOf(T) = 2) then
    Word(Result) := Word(L) - Word(R) // New cast!
  // ...
  else if (GetTypeKind(T) = tkFloat) and (SizeOf(T) = 10) then
    Extended(Result) := Extended(L) - Extended(R); // New cast!
end;
Note:
L-Value cast for generic type requires the 'record' or 'unmanaged' constraints.

See Also