Author: Jonas Bilinkevicius
How to create a type specific list in Delphi without reimplementing the entired
list for each type ?
Answer:
The C++ language has a nice feature, that's called Templates. It allowes the
programer to define a class (or a method) that acts with a none-specific type. At
complie time, the programer describes for which type the class will be defined.
That is, you can define a general list (list template) and define all of it's
methods to work on type A (where 'A' is not defined). For example, the method
GetItem will look as follows :
function GetItem(Index: Integer): A;
Then, at compile type you tell the complier that 'A' is actually an Integer, and
the complier replaces all of the accurances of 'A' with 'Integer'. That way, you
can write one list (for type 'A'), and each time you wan a list (of Strings,
Integers, Boolean, Soubles, etc.) you just need to tell the complier to replace 'A'
with the type you want.
All of that is very nice, but has nothing to do with Delphi. It's relevent only to
C++ programers. So what do Delphi programers do ?
There are 3 majore options. First, write a list of pointers once, and then use it
many times by passing to it a pointer to the datatype you are interested in. For
Example :
1 TList = class
2 ...
3 public
4 procedure Add(Value: Pointer);
5
6 function GetItem(Index: Integer): Pointer;
7 procedure SetItem(Index: Integer; Value: Pointer);
8
9 property Items[Index: Integer]: Pointer read GetItem write SetItem;
10
11 end;
12
13 /Here is the code to use this :
14
15 // For Integer;
16 type
17 PInteger = ^Integer;
18 var
19 Item: PInteger;
20 List: TList;
21 begin
22 List := TList.Create;
23 GetMem(Item, SizeOf(Item));
24 Item^ := 1023; // Or what ever value you wish
25 List.Add(Item);
26 ShowMessage(IntToStr(PInteger(List.Items[0])^));
27 end;
28
29 // For Double;
30 type
31 PDouble = ^Double;
32 var
33 Item: PDouble;
34 List: TList;
35 begin
36 List := TList.Create;
37 GetMem(Item, SizeOf(Item));
38 Item^ := 3.14.15926; // Or what ever value you wish
39 List.Add(Item);
40 ShowMessage(FloatToStr(PDouble(List.Items[0])^));
41 end;
As you've probably noticed there are a few drawbacks to this solution. The most
obvious one is that you need to typecast the value returned by the List each time
you want to use it. That might seem as a mere inconvinouce, but if you plan to uses
lists intensivly, you'll get REALY tired of typecasting all the time. The second
problem to consider with this design is memory concerns. In the example above, I've
allocated memory to Item, but never free it. That's because the TList class I've
used doesn't allocate memory by itself. But then arisses the question, how will
free the memory ? Probably the TList itself (since the item is now 'owned' by it),
but that is a bit unconventional, because usually the object (or method) that
allocates the memory is responsibly to freeing it. You can solve this by writing
the TList class so it allocates it's own memory and only COPIES the value pointed
to by Item. But then there are two other problem.
You need to free the memory of Item after adding it to the List (since the List
isn't going to free it - it only copied the Items contents).
You need to find a way of telling TList how many byte to copy. Since TList gets a
pointer and doesn't know what it points to (a string ? an integer ? a double ?), it
has no way of knowing how many bytes to copy.
Those are all very good reasons why NOT to use this solution. Lets have a look at
the second solution out of the three.
The second solution is very simple. Write a new list for each type. That is, write
a TIntergerList, TStringList, TDoubleList, TWhatEverList. Example :
42 TIntegerList = class
43 ...
44 public
45 procedure Add(Value: Integer);
46
47 function GetItem(Index: Integer): Integer;
48 procedure SetItem(Index: Integer; Value: Integer);
49
50 proepry Items[Idnex: Integer]: Integer read GetItem write SetItem;
51 end;
52
53 TDoubleList = class
54 ...
55 public
56 procedure Add(Value: Double);
57
58 function GetItem(Index: Integer): Double;
59 procedure SetItem(Index: Integer; Value: Double);
60
61 property Items[Index: Integer]: Double read GetItem write SetItem;
62 end;
The benefits are obvious. You can use a list and have no memory problems and you
need not typecast ! Implementing these lists could be a little time consuming, but
if you work a lot with the same types of lists it might be worth while. The only
draw back of this design (except for a one time developing cost) is it's not
extendable (at least not easly). That is, if you want to add a new function to your
List (for example : SaveToFile), you'll have to add the same code for each list you
implement. That vrings us to the third and final solution.
This solution is a combination of the first and second solutions. It tries to take
the best of each. The first solution was very general (worked for every type
without adding code), but you couldn't make it specific (you have to use
typecasting inorder to use an Item). The second solution was very specific (no
typecasting needed) but you had to write a bunch of code for each new list you
wanted to implement.
And here is the third solution : Define a base class that is the same as the TList
in the first solution. Then, for each new list you want (for example :
TIntegerList) smiply inherite from the base class and add the type specific methods
(for example : procedure Add(Value : Integer)). There are a few problems with this
design as well, but I'll discuss them later. For now, lets see why this design
helps as more than the other two.
First, it allows you to use type specific lists (no need for typecasting). Second
it doesn't require you to write a lot of code (five mintues will do) for each new
List because most of the methods are already implemented and the new methods that
need to be implemented are very short.
Lets look closly at the last suggestion. First we need to define a base class :
63 TBaseList = class
64 protected
65 procedure AddData(Value: Pointer);
66 class function ItemSize: Integer; virtual; abstract;
67 end;
68
69 procedure TBaseLink.AddData(Value: Pointer);
70 var
71 P: Pointer;
72 begin
73 GetMem(P, ItemSize);
74 Move(P^, Value^, ItemSize);
75 // Here you need to add P to your list.
76 // The way that is done may vary by the way you decide
77 // to save your data. You may want to save it as an Array
78 // or as a linked list, or as a tree, or into a stream, etc.
79 end;
80
81 //Now, lets create a TIntegerList :
82
83 TIntegerList = class
84 protected
85 class function ItemSize: Integer; override;
86 public
87 procedure Add(Value: Integer);
88 end;
89
90 class fucntion TIntegerList.ItemSize: Integer;
91 begin
92 Result := SizeOf(Integer);
93 end;
94
95 procedure TIntegerList.Add(Value: Integer);
96 var
97 P: ^Integer;
98 begin
99 GetMem(P, SizeOf(Integer));
100 try
101 P^ := Value;
102 AddData(P);
103 finally
104 FreeMem(P, SizeOf(Integer));
105 end;
106 end;
This example is simplefied. In a real list (with full capabilitys) most of the
coding is in the base class, and only a few methods are need to be implemented in
the derived classes.
I've attached a full implementation of this concept for TIntegerList and
TStringList. Notice a few things about the attached file : a) The IBooleanList is
defined but not implemented. b) The marked out methods at the begining of the file
are not implemented yet. c) Objects aren't suported yet.
When I finish coding these lists, I'll write another article describing my specific
implementation of this idea.
Component Download: http://www.baltsoft.com/files/dkb/attachment/lists.ziphttp://www.baltsoft.com/files/dkb/attachment/lists.zip
|