• No results found

Hashed Associative Lists

In document Mastering Delphi 7 (Page 137-150)

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.

Note 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;

Note 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.

Type-Safe Containers and Lists

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.

Note 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);

This document was created by an unregistered ChmMagic, please go to http://www.bisenter.com to register it. Thanks.

function GetObject (Index: Integer): TDate;

TDateListW = class(TObject) private

FList: TObjectList;

function GetObject (Index: Integer): TDate;

procedure SetObject (Index: Integer; Obj: TDate);

function GetCount: Integer;

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.

Note Instead of rewriting wrapper-style list classes for different types, you can use my List Template Wizard. See Appendix A for details.

This document was created by an unregistered ChmMagic, please go to http://www.bisenter.com to register it. Thanks.

Streaming

Another core area of the Delphi class library is its support for streaming, which includes file management, memory, sockets, and other sources of information arranged in a sequence. The idea of streaming is that you move through the data while reading it, much like the Read and Write functions traditionally used by the Pascal language (and discussed in Chapter 12 of Essential Pascal (see Appendix C for availability of this e-book).

The TStream Class

The VCL defines the abstract TStream class and several subclasses. The parent class, TStream, has just a few properties, and you'll never create an instance of it, but it has an interesting list of methods you'll generally use when working with derived stream classes.

The TStream class defines two properties, Size and Position. All stream objects have a specific size (which generally grows if you write something after the end of the stream), and you must specify a position within the stream where you want to either read or write information.

Reading and writing bytes depends on the actual stream class you are using, but in both cases you don't need to know much more than the size of the stream and your relative position in the stream to read or write data. In fact, that's one of the advantages of using streams. The basic interface remains the same whether you're manipulating a disk file, a binary large object (BLOB) field, or a long sequence of bytes in memory.

In addition to the Size and Position properties, the TStream class also defines several important methods, most of which are virtual and abstract. (In other words, the TStream class doesn't define what these methods do; therefore, derived classes are responsible for implementing them.) Some of these methods are important only in the context of reading or writing components within a stream (for instance, ReadComponent and WriteComponent), but some are useful in other contexts, too. In Listing 4.2, you can find the declaration of the TStream class, extracted from the Classes unit.

Listing 4.2: The Public Portion of the Definition of the TStream Class

TStream = class(TObject) public

// read and write a buffer

function Read(var Buffer; Count: Longint): Longint; virtual;abstract;

function Write(const Buffer; Count: Longint): Longint; virtual;abstract;

procedure ReadBuffer(var Buffer; Count: Longint);

procedure WriteBuffer(const Buffer; Count: Longint);

// move to a specific position

function Seek(Offset: Longint; Origin: Word): Longint; overload;virtual;

function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;

overload; virtual;

// copy the stream

function CopyFrom(Source: TStream; Count: Int64): Int64;

// read or write a component

function ReadComponent(Instance: TComponent): TComponent;

function ReadComponentRes(Instance: TComponent): TComponent;

procedure WriteComponent(Instance: TComponent);

procedure WriteComponentRes(const ResName: string; Instance: TComponent);

procedure WriteDescendent(Instance, Ancestor: TComponent);

procedure WriteDescendentRes(

const ResName: string; Instance, Ancestor: TComponent);

procedure WriteResourceHeader(const ResName: string; out FixupInfo: Integer);

procedure FixupResourceHeader(FixupInfo: Integer);

procedure ReadResHeader;

// properties

property Position: Int64 read GetPosition write SetPosition;

property Size: Int64 read GetSize write SetSize64;

end;

The basic use of a stream involves calling the ReadBuffer and WriteBuffer methods, which are very powerful but not terribly easy to use. The first parameter is an untyped buffer in which you can pass the variable to save from or load to. For example, you can save into a file a number (in binary format) and a string, with this code:

var

stream := TFileStream.Create ('c:\tmp\test', fmCreate);

stream.WriteBuffer (n, sizeOf(integer));

stream.WriteBuffer (str[1], Length (str));

stream.Free;

An alternative approach is to let specific components save or load data to and from streams. Many VCL classes define a LoadFromStream or a SaveToStream method, including TStrings, TStringList, TBlobField, TMemoField, TIcon, and TBitmap.

Specific Stream Classes

Creating a TStream instance makes no sense, because this class is abstract and provides no direct support for saving data. Instead, you can use one of the derived classes to load data from or store it to an actual file, a BLOB field, a socket, or a memory block. Use TFileStream when you want to work with a file, passing the filename and some file access options to the Create method. Use TMemoryStream to manipulate a stream in memory and not an actual file.

Several units define TStream-derived classes. The Classes unit includes the following classes:

THandleStream defines a stream that manipulates a disk file represented by a file handle.

TFileStream defines a stream that manipulates a disk file (a file that exists on a local or network disk) represented by a filename. It inherits from THandleStream.

TCustomMemoryStream is the base class for streams stored in memory but is not used directly.

TMemoryStream defines a stream that manipulates a sequence of bytes in memory. It inherits from TCustomMemoryStream.

TStringStream provides a simple way to associate a stream to a string in memory, so that you can access the string with the TStream interface and also copy the string to and from another stream.

TResourceStream defines a stream that manipulates a sequence of bytes in memory, and provides read-only access to resource data linked into the executable file of an application (the DFM files are an example of this resource data). It inherits from TCustomMemoryStream.

Stream classes defined in other units include the following:

TBlobStream defines a stream that provides simple access to database BLOB fields. There are similar BLOB streams for database access technologies other than the BDE, including TSQLBlobStream and TClientBlobStream. (Notice that each type of dataset uses a specific stream class for BLOB fields.) All these classes inherit from TMemoryStream.

TOleStream defines a stream for reading and writing information over the interface for streaming provided by an OLE object.

This document was created by an unregistered ChmMagic, please go to http://www.bisenter.com to register it. Thanks.

TWinSocketStream provides streaming support for a socket connection.

Using File Streams

Creating and using a file stream can be as simple as creating a variable of a type that descends from TStream and calling components' methods to load content from the file:

var

S: TFileStream;

begin

if OpenDialog1.Execute then begin

S := TFileStream.Create (OpenDialog1.FileName, fmOpenRead);

try

Memo1.Lines.LoadFromStream (S);

finally S.Free;

end;

end;

end;

As you can see in this code, the Create method for file streams has two parameters: the name of the file and a flag indicating the requested access mode. In this case, you want to read the file, so you use the fmOpenRead flag (other available flags are documented in the Delphi help).

Note Of the different modes, the most important are fmShareDenyWrite, which you'll use when you're simply reading data from a shared file, and fmShareExclusive, which you'll use when you're writing data to a shared file. There is a third parameter in TFileStream.Create, called Rights. This parameter is used to pass file access permissions to the Linux filesystem when the access mode is fmCreate (that is, only when you are creating a new file). This parameter is ignored on Windows.

A big advantage of streams over other file access techniques is that they're very interchangeable, so you can work with memory streams and then save them to a file, or you can perform the opposite operations. This might be a way to improve the speed of a file-intensive program. Here is a snippet of a file-copying function to give you another idea of how you can use streams:

procedure CopyFile (SourceName, TargetName: String);

var

Stream1, Stream2: TFileStream;

begin

Stream1 := TFileStream.Create (SourceName, fmOpenRead);

try

Stream2 := TFileStream.Create (TargetName, fmOpenWrite or fmCreate);

try

Stream2.CopyFrom (Stream1, Stream1.Size);

finally

Another important use of streams is to handle database BLOB fields or other large fields directly. You can export such data to a stream or read it from one by calling the SaveToStream and LoadFromStream methods of the TBlobField class.

Note Delphi 7 streaming support adds a new exception base class, EFileStreamError. Its constructor takes as parameter a filename for error reporting. This class standardizes and largely simplifies the notification of file-related errors in streams.

The TReader and TWriter Classes

By themselves, the VCL stream classes don't provide much support for reading or writing data. In fact, stream classes don't implement much beyond simply reading and writing blocks of data. If you want to load or save specific data types in a stream (and don't want to perform a great deal of typecasting), you can use the TReader and TWriter classes, which derive from the generic TFiler class.

Basically, the TReader and TWriter classes exist to simplify loading and saving stream data according to its type, and not just as a sequence of bytes. To do this, TWriter embeds special signatures into the stream that specify the type for each object's data. Conversely, the TReader class reads these signatures from the stream, creates the appropriate objects, and then initializes those objects using the subsequent data from the stream.

For example, I could have written out a number and a string to a stream by writing:

var

stream := TFileStream.Create ('c:\tmp\test.txt', fmCreate);

w := TWriter.Create (stream, 1024);

w.WriteInteger (n);

w.WriteString (str);

w.Free;

stream.Free;

This time the file will include the extra signature characters, so I can read back this file only by using a TReader object.

For this reason, using TReader and TWriter is generally confined to component streaming and is seldom applied in general file management.

Streams and Persistency

In Delphi, streams play a considerable role in persistency. For this reason, many methods of TStream relate to saving and loading a component and its subcomponents. For example, you can store a form in a stream by writing

stream.WriteComponent(Form1);

If you examine the structure of a Delphi DFM file, you'll discover that it's really just a resource file that contains a custom format resource. Inside this resource, you'll find the component information for the form or data module and for each of the components it contains. As you would expect, the stream classes provide two methods to read and write this custom resource data for components: WriteComponentRes to store the data, and ReadComponentRes to load it.

For your experiment in memory (not involving DFM files), though, using WriteComponent is generally better suited. After you create a memory stream and save the current form to it, the problem is how to display it. You can do this by transforming the form's binary representation to a textual representation. Even though the Delphi IDE, since version 5, can save DFM files in text format, the representation used internally for the compiled code is invariably a binary format.

The IDE can accomplish the form conversion, generally with the View as Text command of the Form Designer, and in other ways. The Delphi Bin directory also contains a command-line utility, CONVERT.EXE. Within your own code, the standard way to obtain a conversion is to call the specific VCL methods. There are four functions for converting to and from the internal object format obtained by the WriteComponent method:

procedure ObjectBinaryToText(Input, Output: TStream); overload;

procedure ObjectBinaryToText(Input, Output: TStream;

var OriginalFormat: TStreamOriginalFormat); overload;

procedure ObjectTextToBinary(Input, Output: TStream); overload;

procedure ObjectTextToBinary(Input, Output: TStream;

var OriginalFormat: TStreamOriginalFormat); overload;

This document was created by an unregistered ChmMagic, please go to http://www.bisenter.com to register it. Thanks.

Four different functions, with the same parameters and names containing the name Resource instead of Binary (as in ObjectResourceToText), convert the resource format obtained by WriteComponentRes. A final method, TestStreamFormat, indicates whether a DFM is storing a binary or textual representation.

Four different functions, with the same parameters and names containing the name Resource instead of Binary (as in ObjectResourceToText), convert the resource format obtained by WriteComponentRes. A final method, TestStreamFormat, indicates whether a DFM is storing a binary or textual representation.

In document Mastering Delphi 7 (Page 137-150)