Warning: filemtime(): stat failed for /home/developpez/www/developpez-com/upload/sjrdhttp://sjrd.developpez.com/stylesheet.css in /home/developpez/www/developpez-com/template/entete.php on line 405
IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Generics with Delphi 2009 Win32

With, as a bonus, anonymous routines and routine references

Date de publication : November 13th, 2008


II. Daily usage: the example of TList<T>
II-A. A simple code to start with
II-B. Assignments between generic classes
II-C. Methods of TList<T>
II-D. TList<T> and comparers
II-D-1. Writing a comparer using TComparer<T>
II-D-2. Writing a comparer using a simple comparison function


II. Daily usage: the example of TList<T>

Paradoxically, we will begin with the daily usage of a generic class -this is a misuse of language, one should say: generic class template-, instead of the design of such a type. There are several good reasons for this.

Firstly, it is considerably easier to write a class when you have a quite precise idea on how you are going to use it. This is more true yet when you discover a new programming paradigm.

Secondly, the majority of presentations of object oriented programming first explain how to use existing classes, as well.


II-A. A simple code to start with

Let us begin softly. We are going to write a small program listing the squares of integer numbers from 0 to X, X being defined by the user.

Without generics, we would have used a dynamic array (and it would have been far better, keep in mind this is a study case), but we are going to use an integer list.

Here is the code:

program TutoGeneriques;

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes, Generics.Collections;

procedure WriteSquares(Max: Integer);
var
  List: TList<Integer>;
  I: Integer;
begin
  List := TList<Integer>.Create;
  try
    for I := 0 to Max do
      List.Add(I*I);

    for I := 0 to List.Count-1 do
      WriteLn(Format('%d*%0:d = %d', [I, List[I]]));
  finally
    List.Free;
  end;
end;

var
  Max: Integer;
begin
  try
    WriteLn('Please enter a natural number:');
    ReadLn(Max);
    WriteSquares(Max);
  except
    on E:Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;

  ReadLn;
end.
				
What should one notice here?

First of all, of course, the declaration of the variable List, along with the creation of the instance. We have concatenated the actual parameter (or equivalent: it is a type, not a value) to the type name TList, between angle brackets < and >.

You can see that, in order to use a generic class, it is necessary, each time you write its name, to specify a type as a parameter. For now, we will always use a real type there.

Some languages allow type inference, i.e., the compiler guesses the type parameter. It is not the case in Delphi Win32. That is why you cannot write:

var
  List: TList<Integer>;
begin
  List := TList.Create; // <Integer> missing here
  ...
end;
				
The second thing is more important, yet lesser noticeable, since it is actually an absence. Indeed, no cast between an Integer and a Pointer is needed! Convenient, isn't it? And moreover, much more readable.

Another advantage is type safety: it is much better handled with generics. With casts, you always risk mistakes, e.g. adding a pointer to list intended for integers, or vice versa. If you correctly use generics, you will not probably need casts any more, or so few. Thereby, you will get less chances to make mistakes. Moreover, if you try and add a Pointer to an object of class TList<Integer>, the compiler will refuse your code. Before generics, such an error could have been revealed only by an absurd value, maybe months after releasing the software into production.

Note I took the type Integer on purpose, to limit code that is not directly related to the notion of generics, but one could have use any type instead.


II-B. Assignments between generic classes

As you know, when speaking of inheritance, we can assign an instance of a child class to a variable of a parent class, but not the other way. What about it with generics?

Start from the principle that you cannot do anything! Every following examples are invalid, and do not compile:

var
  NormalList: TList;
  IntList: TList<Integer>;
  ObjectList: TList<TObject>;
  ComponentList: TList<TComponent>;
begin
  ObjectList := IntList;
  ObjectList := NormalList;
  NormalList := ObjectList;
  ComponentList := ObjectList;
  ObjectList := ComponentList; // yes, even this is invalid
end;
				
As the comment implies, despite the fact that a TComponent may be assigned to a TObject, a TList<TComponent> may not be assigned to a TList<TObject>. In order to understand why, just think that TList<TObject>.Add(Value: TObject) would allow, if the assignment was valid, to insert a TObject value in a TComponent list!

The other important thing to notice is that TList<T> is not a specialization of TList, neither a generalization. Actually, they are totally different types, the former one being declared in the Generics.Collections unit, and the latter in Classes!


II-C. Methods of TList<T>

Interrestingly, TList<T> does not provide the same set of methods as TList does. We have more "high-level" methods at our disposal, but less "low-level" methods (as Last, which is missing).

The new methods are the following:

  • 3 overloaded versions of AddRange, InsertRange and DeleteRange: they are equivalent to Add/Insert/Delete respectively, but for a list of elements;
  • A Contains method;
  • A LastIndexOf method;
  • A Reverse method;
  • 2 overloaded versions of Sort (whose name is not new, but whose usage is quite differente);
  • 2 overloaded versions of BinarySearch.
I draw your attention to those changes, which you may find insignificant, because they are quite characteristic of the general changes in design concerns braught by generics.

What is the purpose, indeed, of implementing an AddRange method in the from now on obsolete TList class? None, since every item would have been cast, in turn. Therefore, one would have written a loop anyway, in order to build the array to insert. One might as well call Add in each loop iteration.

Meanwhile, with generics, the code can be written once and for all, and it becomes truly useful, for each and every type.

What you should remark and understand here, is that generics allow much more factoring of behaviors.


II-D. TList<T> and comparers

Certainly, TList<T> can handle any type. But how can it know how to compare two elements? How to know if they are equal, in order to search with IndexOf? How to know if one is lesser than another, in order to sort the list?

The answer is comparers. A comparer is an interface of type IComparer<T>. So yes, we are staying in the middle of generics. This type is declared in Generics.Defaults.pas.

When you instanciate a TList<T>, you may pass a comparer to the constructor, which will be used by all methods that need it. If you do not, a default comparer will be used.

The default comparer depends on the element type, of course. To get it, TList<T> calls the class method TComparer<T>.Default. This method does some nasty work, based on RTTI, to get the best possible solution. But does not always fit the requirements.

You may use the default comparer for the following data types:

  • Ordinal types (integers, characters, booleans, enumerations);
  • Floating point types;
  • Set types (only for equality);
  • Unicode long strings (string, UnicodeString and WideString);
  • ANSI long strings (AnsiString), but without page code for < and >;
  • Variant types (only for equality);
  • Class types (only for equality - uses the TObject.Equals method);
  • Pointer, meta-class and interface types (only for equality);
  • Static and dynamic arrays whose elements are ordinal, floating point or set types (only for equality).
For all other types, the default comparer compares only the memory contents of the variable. One should therefore write a custom comparer.

There exist two simple ways. The first one is based on writing a function, the other one on the derivation of the class TComparer<T>. We will illustrate both of them by implementing comparison for TPoint. We will assume that points are compared according to their distance to the origin -the point (0, 0)- in order to have a total ordering (in the mathematical meaning).


II-D-1. Writing a comparer using TComparer<T>

Nothing easier, you have always done that! An only method to override: Compare. It must return 0 on equality, a positive integer if the first parameter is greater than the second one, and a negative integer otherwise.

Here is the result:

function DistanceToCenterSquare(const Point: TPoint): Integer; inline;
begin
  Result := Point.X*Point.X + Point.Y*Point.Y;
end;

type
  TPointComparer = class(TComparer<TPoint>)
    function Compare(const Left, Right: TPoint): Integer; override;
  end;

function TPointComparer.Compare(const Left, Right: TPoint): Integer;
begin
  Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);
end;
					
info Notice the parent class of TPointComparer: it inherits from TComparer<TPoint>. You can see that it is possible for a "simple" class to inherit from generic class, provided it is given an actual parameter for its generic parameter.
In order to use our comparer, just instantiate it and pass the instance to the list constructor. Here is a small program that creates 10 random points, sort them and prints the sorted list.

function DistanceToCenter(const Point: TPoint): Extended; inline;
begin
  Result := Sqrt(DistanceToCenterSquare(Point));
end;

procedure SortPointsWithTPointComparer;
const
  MaxX = 100;
  MaxY = 100;
  PointCount = 10;
var
  List: TList<TPoint>;
  I: Integer;
  Item: TPoint;
begin
  List := TList<TPoint>.Create(TPointComparer.Create);
  try
    for I := 0 to PointCount-1 do
      List.Add(Point(Random(2*MaxX+1) - MaxX, Random(2*MaxY+1) - MaxY));

    List.Sort; // uses the comparer given to the constructor

    for Item in List do
      WriteLn(Format('%d'#9'%d'#9'(distance to origin = %.2f)',
        [Item.X, Item.Y, DistanceToCenter(Item)]));
  finally
    List.Free;
  end;
end;

begin
  try
    Randomize;

    SortPointsWithTPointComparer;
  except
    on E:Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;

  ReadLn;
end.
					
warning Wait a minute! Where is the comparer being freed? Recall that TList<T> takes an interface of type IComparer<T>, not a class. Because of reference counting on interfaces (implemented correctly in TComparer<T>), the comparer will be automatically freed when it is no longer needed.
If you do not know anything about interfaces in Delphi, I recommand you read the (French-speaking) tutorial fr Les interfaces d'objet sous Delphi written by Laurent Dardenne.

II-D-2. Writing a comparer using a simple comparison function

This alternative seems to be simpler, given its name: no need to play with additional classes. However, I have chosen to present it second, because it introduces a new data type available in Delphi 2009. I am speaking of routine references.

Er ... I know routine references! Hum, well, no, you do not ;-) What you already know are procedural types, for example TNotifyEvent:

type
  TNotifyEvent = procedure(Sender: TObject) of object;
					
Routine reference types are declared, in this example, like TComparison<T>:

type
  TComparison<T> = reference to function(const Left, Right: T): Integer;
					
There are at least three differences between procedural types and routine reference types.

Firstly, a routine reference type cannot be marked as of object. In other words, you can never assign a method to a routine reference, only... routines. (Or, at least, I have not been successful in trying to do so ^^.)

The second difference is more fundamental: while a procedural type (non of object) is a pointer to the base address of a routine (its entry point), a routine reference type is actually an interface! With reference counting and this kind of things. However, you probably will never have to care about that, because its daily usage is identical to that of a procedural type.

Lastly -and this explains the apparition of routine references-, one can assign a anonymous routine (we will see in a moment what it is like)- to a routine reference, but not to a procedural type. Try to do so, you will soon see that the compiler reports errors. Incidentally, that explains also why routine references are implemented as interfaces, but thoughts on this subject are not within the framework of this tutorial.

Let us get back to our point sorting. In order to create a comparer on the basis of a function, we use another class method of TComparer<T>; it is Construct. This class method takes as parameter a routine reference of type TComparison<T>. As was already said, using routine references is quite similar to the use of procedural types: one may use the routine name as parameter, directly. Here is the code:

function ComparePoints(const Left, Right: TPoint): Integer;
begin
  Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);
end;

procedure SortPointsWithComparePoints;
const
  MaxX = 100;
  MaxY = 100;
  PointCount = 10;
var
  List: TList<TPoint>;
  I: Integer;
  Item: TPoint;
begin
  List := TList<TPoint>.Create(TComparer<TPoint>.Construct(ComparePoints));
  try
    for I := 0 to PointCount-1 do
      List.Add(Point(Random(2*MaxX+1) - MaxX, Random(2*MaxY+1) - MaxY));

    List.Sort; // uses the comparer given to the constructor

    for Item in List do
      WriteLn(Format('%d'#9'%d'#9'(distance to origin = %.2f)',
        [Item.X, Item.Y, DistanceToCenter(Item)]));
  finally
    List.Free;
  end;
end;

begin
  try
    Randomize;

    SortPointsWithComparePoints;
  except
    on E:Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;

  ReadLn;
end.
					
The only difference coming, of course, from the creation of the comparer. The rest of the usage of the list is strictly identical (just as well!).

Internally, the class method Construct creates an instance of TDelegatedComparer<T>, which takes as parameter of its constructor the routine reference which is going to handle the comparison. Calling Construct thus returns an object of this type, encapsulated in an interface IComparer<T>.

Well, it was finally easier as well. Actually, on should realize it: generics are there to ease our life!

But I have let something go: one can assign a anonymous routine to a routine reference. So, let us see what would it be like:

procedure SortPointsWithAnonymous;
var
  List: TList<TPoint>;
  // ...
begin
  List := TList<TPoint>.Create(TComparer<TPoint>.Construct(
    function(const Left, Right: TPoint): Integer
    begin
      Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);
    end));

  // Always the same ...
end;
					
This kind of create is interresting mostly if it is the only place where you need the comparison routine.

info Speaking of anonymous routines: yes, they may access local variables of the enclosing routine/method. And yes, they may do so even after the enclosing routine/method has returned. The following example illustrates it all:

function MakeComparer(Reverse: Boolean = False): TComparison<TPoint>;
begin
  Result :=
    function(const Left, Right: TPoint): Integer
    begin
      Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);
      if Reverse then
        Result := -Result;
    end;
end;

procedure SortPointsWithAnonymous;
var
  List: TList<TPoint>;
  // ...
begin
  List := TList<TPoint>.Create(TComparer<TPoint>.Construct(
    MakeComparer(True)));

  // Always the same ...
end;
					
Interresting, isn't it?

Voilà! Here is the end of our small tour of comparers used with TList<T>, and, with it, this introduction to generics through the use of this class. In the following chapter, we will begin to learn how one can write his own generic class.

 

Warning: include(): http:// wrapper is disabled in the server configuration by allow_url_include=0 in /home/developpez/www/developpez-com/upload/sjrd/delphi/tutoriel/generics/index.php on line 41

Warning: include(http://sjrd.developpez.com/references.inc): failed to open stream: no suitable wrapper could be found in /home/developpez/www/developpez-com/upload/sjrd/delphi/tutoriel/generics/index.php on line 41

Warning: include(): Failed opening 'http://sjrd.developpez.com/references.inc' for inclusion (include_path='.:/opt/php56/lib/php') in /home/developpez/www/developpez-com/upload/sjrd/delphi/tutoriel/generics/index.php on line 41

Valid XHTML 1.0 TransitionalValid CSS!

Copyright © 2008-2009 Sébastien Doeraene. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.