Generic Types

A Class, Record or Interface can be generic, if it operates on one or more types that are not specified in concrete when the type is defined.

Why Generics?

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 Parameters

Any Class, Record or Interface type declaration can be made generic by applying one or more type parameters to its name, enclosed in angle brackets:

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 T, U, 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 T.

This is where constraints come in.

Constraints

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 where keyword.

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 Value Type (i.e. disallows classes).
  • is   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;

The 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 IComparable).

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 out or 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 IReadOnlyList<Object>.

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 Object.

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 IWriteOnlyList<Button>.

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.

.NET Only

Co- and Contra-Variance is supported on the .NET platform only.

See Also