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:
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.
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;
Name-Value Pairs (and Delphi 7 Extensions)
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
Using Lists of Objects
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:
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.
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:
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.
Hashed Associative Lists
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.
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;
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:
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.
|Copyright © 2004-2021 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide||