It is often important to handle groups of components or objects. In addition to using standard arrays and dynamic arrays, a few VCL classes represent lists of other objects. These classes can be divided into three groups: simple lists, collections, and containers.
Lists are represented by the generic list of objects, TList, and by the two lists of strings, TStrings and TStringList:
TList defines a list of pointers, which can be used to store objects of any class. A TList is more flexible than a dynamic array, because it can be expanded automatically simply by adding new items to it. The advantage of a dynamic array over a TList is that the dynamic array allows you to indicate a specific type for contained objects and perform the proper compile-time type checking.
TStrings is an abstract class to represent all forms of string lists, regardless of their storage implementations. This class defines an abstract list of strings. For this reason, TStrings objects are used only as properties of components capable of storing the strings themselves, such as a list box.
TStringList, a subclass of TStrings, defines a list of strings with their own storage. You can use this class to define a list of strings in a program.
TStringList and TStrings objects have both a list of strings and a list of objects associated with the strings. These classes have a number of different uses. For example, you can use them for dictionaries of associated objects or to store bitmaps or other elements to be used in a list box.
The two classes of string lists also have ready-to-use methods to store or load their contents to or from a text file: SaveToFile and LoadFromFile. To loop through a list, you can use a simple for statement based on its index, as if the list were an array.
All these lists have a number of methods and properties. You can operate on lists using the array notation ([ and ]) both to read and to change elements. There is a Count property, as well as typical access methods, such as Add, Insert, Delete, Remove; search methods (for example, IndexOf); and sorting support. The TList class has an Assign method that, besides copying the source data, can perform set operations on the two lists, including and, or, and xor.
To fill a string list with items and later check whether one is present, you can write code like this:
var sl: TStringList; idx: Integer; begin sl := TStringList.Create; try sl.Add ('one'); sl.Add ('two'); sl.Add ('three'); // later idx := sl.IndexOf ('two'); if idx >= 0 then ShowMessage ('String found'); finally sl.Free; end; end;
The TStringList class has always had another nice feature: support for name-value pairs. If you add to a list a string like 'lastname=john', you can then search for the existence of the pair using the IndexOfName function or the Values array property. For example, you can retrieve the value 'john' by calling Values ['lastname'].
You can use this feature to build much more complex data structures, such as dictionaries, and still benefit from the possibility of attaching an object to the string. This data structure maps directly to initialization files and other common formats.
Delphi 7 further extends the possibilities of name-value pair support by allowing you to customize the separator, beyond the equal sign, using the new NameValueSeparator property. In addition, the new ValueFromIndex property gives you direct access to the value portion of a string at a given position; you no longer have to extract the name value manually from the complete string using a cumbersome (and extremely slow) expression:
str := MyStringList.Values [MyStringList.Names [I]]; // old str := MyStringList.ValueFromIndex [I]; // new
I've written an example focusing on the use of the generic TList class. When you need a list of any kind of data, you can generally declare a TList object, fill it with the data, and then access the data while casting it to the proper type. The ListDemo example demonstrates this approach and also shows its pitfalls. The form has a private variable, holding a list of dates:
private ListDate: TList;
This list object is created when the form itself is created:
procedure TForm1.FormCreate(Sender: TObject); begin Randomize; ListDate := TList.Create; end;
A button on the form adds a random date to the list (of course, I've included in the project the unit containing the date component built in the previous chapter):
procedure TForm1.ButtonAddClick(Sender: TObject); begin ListDate.Add (TDate.Create (1900 + Random (200), 1 + Random (12), 1 + Random (30))); end;
When you extract the items from the list, you have to cast them back to the proper type, as in the following method, which is connected to the List button (you can see its effect in Figure 4.4):
procedure TForm1.ButtonListDateClick(Sender: TObject); var I: Integer; begin ListBox1.Clear; for I := 0 to ListDate.Count - 1 do Listbox1.Items.Add ((TObject(ListDate [I]) as TDate).Text); end;
At the end of the code, before you can do an as downcast, you first need to hard-cast the pointer returned by the TList into a TObject reference. This kind of expression can result in an invalid typecast exception, or it can generate a memory error when the pointer is not a reference to an object.
If there were no possibility of having anything but date objects in the list, extracting it with a static cast rather than an as cast would be more efficient. However, when there's even a remote chance of having a wrong object, I'd suggest using the as cast.
To demonstrate that things can indeed go wrong, I've added one more button, which adds a TButton object to the list by calling ListDate.Add(Sender). If you click this button and then update one of the lists, you'll get an error. Finally, remember that when you destroy a list of objects, you should destroy all of the list's objects first. The ListDemo program does this in the form's FormDestroy method:
procedure TForm1.FormDestroy(Sender: TObject); var I: Integer; begin for I := 0 to ListDate.Count - 1 do TObject(ListDate [I]).Free; ListDate.Free; end;
The second group, collections, contains only two VCL classes: TCollection and TCollectionItem. TCollection defines a homogeneous list of objects, which are owned by the collection class. The objects in the collection must be descendants of the TCollectionItem class. If you need a collection storing specific objects, you have to create both a subclass of TCollection and a matching subclass of TCollectionItem.
Collections are used to specify values of component properties; it is very unusual to work with collections to store your own objects. I discuss collections in Chapter 9.
Recent versions of Delphi include a series of container classes, defined in the Contnrs unit. The container classes extend the TList classes by adding the idea of ownership and by defining specific extraction rules (mimicking stacks and queues) or sorting capabilities.
The basic difference between TList and the new TObjectList class, for example, is that the latter is defined as a list of TObject objects, not a list of pointers. Even more important, however, is the fact that if the object list has the OwnsObjects property set to True, it automatically deletes an object when it is replaced by another one and deletes each object when the list itself is destroyed. Here's a list of the container classes:
The TObjectList class (already described) represents a list of objects, eventually owned by the list itself.
The inherited class TComponentList represents a list of components, with full support for destruction notification (an important safety feature when two components are connected using their properties; that is, when a component is the value of a property of another component).
The TClassList class is a list of class references. It inherits from TList and requires no specific destruction, as you don't have to destroy class references in Delphi.
The classes TStack and TObjectStack represent lists of pointers and objects, from which you can only extract elements starting from the last one you've inserted. A stack follows LIFO order (last in, first out). The typical methods of a stack are Push for insertion, Pop for extraction, and Peek to preview the first item without removing it. You can still use all the methods of the base class, TList.
The classes TQueue and TObjectQueue represent lists of pointers and objects, from which you always remove the first item you've inserted (FIFO: first in, first out). The methods of these classes are the same as those of the stack classes, but they behave differently.
Unlike TObjectList, the TObjectStack and TObjectQueue classes do not own the inserted objects and will not destroy those objects left in the data structure when it is destroyed. You can simply Pop all the items, destroy them once you're finished using them, and then destroy the container.
To demonstrate the use of these classes, I've modified the earlier ListDate example into the Contain example. First, I changed the type of the ListDate variable to TObjectList. In the FormCreate method, I've modified the list creation to the following code, which activates the list ownership:
ListDate := TObjectList.Create (True);
At this point, you can simplify the destruction code, because applying Free to the list will automatically free the dates it holds.
I've also added to the program a stack and a queue object, filling each of them with numbers. One of the form's two buttons displays a list of the numbers in each container, and the other removes the last item (displayed in a message box):
procedure TForm1.btnQueueClick(Sender: TObject); var I: Integer; begin ListBox1.Clear; for I := 0 to Stack.Count - 1 do begin ListBox1.Items.Add (IntToStr (Integer (Queue.Peek))); Queue.Push(Queue.Pop); end; ShowMessage ('Removed: ' + IntToStr (Integer (Stack.Pop))); end;
By clicking the two buttons, you can see that calling Pop for each container returns the last item. The difference is that the TQueue class inserts elements at the beginning, and the TStack class inserts them at the end.
Since Delphi 6, the set of predefined container classes includes TBucketList and TObjectBucketList. These two lists are associative, which means they have a key and an actual entry. The key is used to identify the items and search for them. To add an item, you call the Add method with two parameters: the key and the data. When you use the Find method, you pass the key and retrieve the data. The same effect is achieved by using the Data array property, passing the key as parameter.
These lists are based on a hash system. The lists create an internal array of items, called buckets, each having a sublist of list elements. As you add an item, its key value is used to compute the hash value, which determines the bucket to which the item should be added. When searching the item, the hash is computed again, and the list immediately grabs the sublist containing the item and searches for it there. This process makes for very fast insertion and searches, but only if the hash algorithm distributes the items evenly among the various buckets and if there are enough different entries in the array. When many elements can be in the same bucket, searching is slower.
For this reason, as you create the TObjectBucketList, you can specify the number of entries for the list by using the parameter of the constructor and choosing a value between 2 and 256. The value of the bucket is determined by taking the first byte of the pointer (or number) passed as the key and doing an and operation with a number corresponding to the entries.
I don't find this algorithm very convincing for a hash system, but replacing it with your own implies only overriding the BucketFor virtual function and eventually changing the number of entries in the array, by setting a different value for the BucketCount property.
Another interesting feature, not available for lists, is the ForEach method, which allows you to execute a given function on each item contained in the list. You pass the ForEach method a pointer to your data and a procedure that receives four parameters: your custom pointer, each key and object of the list, and a Boolean parameter you can set to False to stop the execution. In other words, these are the two signatures:
type TBucketProc = procedure(AInfo, AItem, AData: Pointer; out AContinue: Boolean); function TCustomBucketList.ForEach(AProc: TBucketProc; AInfo: Pointer): Boolean;
In addition to these containers, Delphi includes a THashedStringList class, which inherits from TStringList. This class has no direct relationship with the hashed lists and is defined in a different unit, IniFiles. The hashed string list has two associated hash tables (of type TStringHash), which are completely refreshed every time the content of the string list changes. So, this class is useful only for reading a large set of fixed strings, not for handling a list of strings changing often over time. On the other hand, the TStringHash support class seems to be quite useful in general cases, and has a good algorithm for computing the hash value of a string.
Containers and lists have a problem: They are not type-safe, as I've shown in both examples by adding a button object to a list of dates. To ensure that the data in a list is homogenous, you can check the type of the data you extract before you insert it, but as an extra safety measure you might also want to check the type of the data while extracting it. However, adding run-time type checking slows a program and is risky—a programmer might fail to check the type in some cases.
To solve both problems, you can create specific list classes for given data types and fashion the code from the existing TList or TObjectList class (or another container class). There are two approaches to accomplish this:
Derive a new class from the list class and customize the Add method and the access methods, which relate to the Items property. This is also the approach used by Borland for the container classes, which all derive from TList.
Delphi container classes use static overrides to perform simple type conveniences (parameters and function results of the desired type). Static overrides are not the same as polymorphism; someone using a container class via a TList variable will not be calling the container's specialized functions. Static override is a simple and effective technique, but it has one very important restriction: The methods in the descendent should not do anything beyond simple typecasting, because you have no guarantee the descendent methods will be called. The list might be accessed and manipulated using the ancestor methods as much as by the descendent methods, so their operations must be identical. The only difference is the type used in the descendent methods, which allows you to avoid extra typecasting.
Create a brand-new class that contains a TList object, and map the methods of the new class to the internal list using proper type checking. This approach defines a wrapper class, a class that "wraps" around an existing one to provide a different or limited access to its methods (in our case, to perform a type conversion).
I've implemented both solutions in the DateList example, which defines lists of TDate objects. In the code that follows, you'll find the declaration of the two classes, the inheritance-based TDateListI class and the wrapper class TDateListW:
type // inheritance-based TDateListI = class (TObjectList) protected procedure SetObject (Index: Integer; Item: TDate); function GetObject (Index: Integer): TDate; public function Add (Obj: TDate): Integer; procedure Insert (Index: Integer; Obj: TDate); property Objects [Index: Integer]: TDate read GetObject write SetObject; default; end; // wrapper based TDateListW = class(TObject) private FList: TObjectList; function GetObject (Index: Integer): TDate; procedure SetObject (Index: Integer; Obj: TDate); function GetCount: Integer; public constructor Create; destructor Destroy; override; function Add (Obj: TDate): Integer; function Remove (Obj: TDate): Integer; function IndexOf (Obj: TDate): Integer; property Count: Integer read GetCount; property Objects [Index: Integer]: TDate read GetObject write SetObject; default; end;
Obviously, the first class is simpler to write—it has fewer methods, and they just call the inherited ones. The good thing is that a TDateListI object can be passed to parameters expecting a TList. The problem is that the code that manipulates an instance of this list via a generic TList variable will not be calling the specialized methods, because they are not virtual and might end up adding to the list objects of other data types.
Instead, if you decide not to use inheritance, you end up writing a lot of code; you need to reproduce every one of the original TList methods, simply calling the methods of the internal FList object. The drawback is that the TDateListW class is not type compatible with TList, which limits its usefulness. It can't be passed as parameter to methods expecting a TList.
Both of these approaches provide good type checking. After you've created an instance of one of these list classes, you can add only objects of the appropriate type, and the objects you extract will naturally be of the correct type. This technique is demonstrated by the DateList example. This program has a few buttons, a combo box to let a user choose which of the lists to show, and a list box to show the list values. The program stretches the lists by trying to add a button to the list of TDate objects. To add an object of a different type to the TDateListI list, you can convert the list to its base class, TList. This might happen accidentally if you pass the list as a parameter to a method that expects an ancestor class of the list class. In contrast, for the TDateListW list to fail, you must explicitly cast the object to TDate before inserting it, something a programmer should never do:
procedure TForm1.ButtonAddButtonClick(Sender: TObject); begin ListW.Add (TDate(TButton.Create (nil))); TList(ListI).Add (TButton.Create (nil)); UpdateList; end;
The UpdateList call triggers an exception, displayed directly in the list box, because I've used an as typecast in the custom list classes. A wise programmer should never write the previous code. To summarize, writing a custom list for a specific type makes a program much more robust. Writing a wrapper list instead of one that's based on inheritance tends to be a little safer, although it requires more coding.
Instead of rewriting wrapper-style list classes for different types, you can use my List Template Wizard. See Appendix A for details.