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 five types of supported constraints:

  • is   TypeName — requires the concrete type to implement the specified Interface or descend from the specified Class.
  • 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 unmanaged — (.NET only) requires the concrete type to be a simple unmananaged1 type.
  • 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, as shown in the next section.

A single where clause may list multiple constrains, separated by comma. Alternatively. multiple where clauses, each terminated with a semicolon, are also permissible.

type
  List<T> = public class
    where T is IComparable;
    where T has constructor;
  ...

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.

Unconstrained Generics

By default, Generics defined on Oxygene are always constrained to be compatible with the default Object type (or any type that can be boxed into an Object).

On platforms that support more than one Object Model, the unconstrained keyword can be used to explicitly mark a generic as supporting all types of object models. Note that this severely restricts what can be done with the elements without requiring explicit casts or use of the modelOf() system function.

type
  List<T> = public class
    where T is unconstrained;

    ...
  end;

See Also


  1. In order to qualify as "unmanaged", a type must an Integer, Float, Char, Boolan, Decimal, Enum or Pointer type or a user-defined Record where each field satisfies same requirement.