Expert Delphi
上QQ阅读APP看书,第一时间看更新

Generics

One of the most powerful concepts in the Object Pascal language is a generic type. This way, we can write our code in a more generic way, so the same algorithm can operate not on just one data type, but many. Generics are things that can be parameterized by type. The code is not fully specified, providing the implementation details to the code that uses generics. There could be generic types where the whole type definition is parameterized by an unknown type, or we can just define generic methods that operate on a type that is not fully specified.

As an example, let's consider a fillable class. Many languages have the concept of nullable values, but we will be different and use "fillable". It seems more natural. One useful example of using nullables or fillables is mapping between the data stored in a relational database and the entities in our code. A field in a database record may contain a value, such as a string or an integer, or it might be null. In order to properly represent the data coming from a database in our code, we need an extra logical flag that will say whether a value of the given type contains a valid value or null. If we represented this data using an object, we would have the possibility to have a nil reference, but it is more efficient to work with plain, simple built-in types the  lifetime of which does not need to be directly managed. Without generics, we would need to implement a fillable class for every field type duplicating the same logic for clearing the flag, returning information if the value is filled or adding two fillables together:

type 
  TFillable<T> = record 
    Value: T; 
    IsFilled: boolean; 
  end; 
 
  TFillableString = TFillable<string>; 
  TFillableInteger = TFillable<integer>; 
  // ... 

If our implementation cannot deal with arbitrary types, we can specify a constraint on a type parameter using a colon. We can, for example, say that a generic type TFmxProcessor can be parameterized by any type, but it needs to be a class derived from TFMXObject, and it needs to have a public constructor:

type 
  TFmxProcessor<T: TFMXObject, constructor> = class 
   // ... 
  end; 
 
  TRecordReporter = class 
    procedure DoReport<T: record>(x: T); 
  end; 

Another example where generics are useful is a custom sorting algorithm. There are many possible implementations, such as bubble sort or quick sort. It does not matter if we are sorting characters, integers, or real numbers. The algorithm's logic is the same. With generics, you do not need to implement the sorting algorithm for all the possible types it can operate on. You just need to have a way to compare two values, so they can be ordered properly.

Delphi comes with the System.Generics.Collections unit, which defines many useful generic collection types, such as enumerations, lists, and dictionaries that we can use in our code.

Consider the following TPerson class:

unit uPerson; 
 
interface 
 
type 
  TPerson = class 
    FirstName, LastName: string; 
    constructor Create(AFirstName, ALastName: string); 
    function Fullname: string; 
  end; 
 
implementation 
 
{ TPerson } 
 
constructor TPerson.Create(AFirstName, ALastName: string); 
begin 
  FirstName := AFirstName; 
  LastName := ALastName; 
end; 
 
function TPerson.Fullname: string; 
begin 
  Result := FirstName + ' ' + LastName; 
end; 
 
end. 

Before the introduction of generics, you could manage a list of objects with the TList class. Let's check out the differences between managing lists with and without generics.

Here is some code that iterates through an object list of the TPerson instance and logs their full names using TList, as shown in the following code snippet:

procedure DoPersonsTList; 
var 
  persons: TList; p: TPerson; i: integer; 
begin 
  persons := TList.Create; 
  try 
    // not safe, can add any pointer 
    persons.Add(TPerson.Create('Kirk', 'Hammett')); 
    persons.Add(TPerson.Create('James', 'Hetfield')); 
    persons.Add(TPerson.Create('Lars', 'Ulrich')); 
    persons.Add(TPerson.Create('Robert', 'Trujillo')); 
 
    for i := 0 to persons.Count-1 do 
    begin 
      p := persons.Items[i]; 
      Log(p.Fullname); 
    end; 
  finally 
    persons.Free; 
  end; 
end; 

TList defined in the System.Classes unit can be used to manage a list of pointers of an arbitrary type. In order to access the Fullname method of the TPerson class, we need to perform a typecast by assigning a pointer reference to a variable of a proper type. If the object is not TPerson or its descendant, we will get an error at runtime, as shown in the following code snippet:

procedure DoPersonsGenerics; 
var  
  persons: TObjectList<TPerson>; p: TPerson; 
begin 
  persons := TObjectList<TPerson>.Create; 
  try 
    // safe, can only add TPerson or descendant 
    persons.Add(TPerson.Create('Kirk', 'Hammett')); 
    persons.Add(TPerson.Create('James', 'Hetfield')); 
    persons.Add(TPerson.Create('Lars', 'Ulrich')); 
    persons.Add(TPerson.Create('Robert', 'Trujillo')); 
 
    for p in persons  do 
      Log(p.Fullname); // no typecast needed 
 
  finally 
    persons.Free; 
  end; 
end; 

Using a generic list is much cleaner. The compiler, at compile time, knows that it deals with a list of TPerson objects and will not compile code that tries to add incompatible references. We can use the more readable for..in..do loop, and there is no need for a typecast. Using generics, in general, improves the quality of your code.