Tuesday, April 8, 2014

Delphi - Records

What are records?
Records are a useful and distinguishing feature of delphi. They provide a very neat way of having named data structures - groups of data fields. Unlike arrays, a record may contain different types of data.

Records are fixed in size - the definition of a record must contain fixed length fields. We are allowed to have strings, but either their length must be specified (for example a : String[20]), or a pointer to the string is stored in the record. In this case, the record cannot be used to write the string to a file. The TPoint type is an example of a record. Before we go any further, let us look at a simple example.
 type
   TCustomer = record
     name : string[30];
     age  : byte;
   end;
  

 var
   customer : TCustomer;
 

 begin
   // Set up our customer record
   customer.name := 'Fred Bloggs';
   customer.age  := 23;
 end;
When we define a record, each field is simply accessed by name, separated by a dot from the record variable name. This makes records almost self documenting, and certainly easy to understand.
Above, we have created one customer, and set up the customer record fields.
Using the with keyword
type
   TCustomer = record
     name : string[30];
     age  : byte;
   end;
 
 
 var
   John, Nancy : TCustomer;
 
 
 begin
   // Set up our customer records
   with John do
   begin
     name := 'John Moffatt';               // Only refer to the record fields
     age  := 67;
   end;
 
 
   with Nancy do
   begin
     name := 'Nancy Moffatt';              // Only refer to the record fields
     age  := 77;
   end;
 end;
A more complex example
In practice, records are often more complex. Additionally, we may also have a lot of them, and might store them in an array. The following example is a complete program that you may copy and paste into your Delphi product, making sure to follow the instructions at the start of the code.

Please note that this is quite a complex piece of code - it uses a procedure that takes a variable number of parameters, specially passed in square brackets (see Procedure for more on procedures).
// Full Unit code.
 // -----------------------------------------------------------
 // You must store this code in a unit called Unit1 with a form
 // called Form1 that has an OnCreate event called FormCreate.
 
 unit Unit1;
  

 interface
  

 uses
   Forms, Dialogs;
  

 type
   TForm1 = class(TForm)
     procedure FormCreate(Sender: TObject);
     procedure ShowCustomer(const fields: array of string);
   end;
 
 
 var
   Form1: TForm1;
 
 
 implementation
 {$R *.dfm} // Include form definitions
 
 
 procedure TForm1.FormCreate(Sender: TObject);
 type
   // Declare a customer record
   TCustomer = Record
     firstName : string[20];
     lastName  : string[20];
     address1  : string[100];
     address2  : string[100];
     address3  : string[100];
     city      : string[20];
     postCode  : string[8];
   end;
  

 var
   customers : array[1..3] of TCustomer;
   i : Integer;
 
 
 begin
   // Set up the first customer record
   with customers[1] do
   begin
     firstName := 'John';
     lastName  := 'Smith';
     address1  := '7 Park Drive';
     address2  := 'Branston';
     address3  := 'Grimworth';
     city      := 'Banmore';
     postCode  := 'BNM 1AB';
   end;
  

   // Set up the second and third by copying from the first
   customers[2] := customers[1];
   customers[3] := customers[1];
 
 
   // And then changing the first name to suit in each case
   customers[2].firstName := 'Sarah';
   customers[3].firstName := 'Henry';
  

   // Now show the details of these customers
   for i := 1 to 3 do
     with customers[i] do ShowCustomer([firstName,
                                        lastName,
                                        address1,
                                        address2,
                                        address3,
                                        city,
                                        postCode]);
 end;
  

 // A procedure that displays a variable number of strings
 procedure TForm1.ShowCustomer(const fields: array of string);
 var
   i : Integer;
 
 
 begin
   // Display all fields passed - note : arrays start at 0
   for i := 0 to Length(fields)-1 do
     ShowMessage(fields[i]);
 
 
   ShowMessage('');
 end;
  

 end.
Packing record data
By default, Delphi will pad out the record with fillers, where necessary, to make sure that fields are aligned on 2, 4 or 8 byte boundaries to improve performance. You can pack the data with the packed keyword to reduce the record size if this is more important than performance. See Packed for more on this topic.

Records with variant parts
Things get very interesting now. There are times when a fixed format record is not useful. First, we may wish to store data in the record in different ways. Second, we may want to store different types of data in a part of a record.
The Delphi TRect type illustrates the first concept. It is defined like this:
type 
   TRect = packed record
     case Integer of
       0: (Left, Top, Right, Bottom: Integer);
       1: (TopLeft, BottomRight: TPoint);
   end; 
Here we have a record that holds the 4 coordinates of a rectangle. The Case clause tells Delphi to map the two following sub-sections onto the same area (the end) of the record. These variant sections must always be at the end of a record. Note also that the case statement has no end statement. This is omitted because the record finishes at the same point anyway.
The record allows us to store data in two ways:
var
   rect1, rect2 : TRect;
 begin
   // Setting up using integer coordinates
   rect1.Left   := 11;
   rect1.Top    := 22;
   rect1.Right  := 33;
   rect1.Bottom := 44;
 

   // Seting up rect2 to have the same coordinates, but using points instead
   rect2.TopLeft     := Point(11,22);
   rect2.BottomRight := Point(33,44);
 end;
The TRect record showed two methods of reading from and writing to a record. The second concept is to have two or more record sub-sections that have different formats and lengths.
This time we will define a fruit record that has a different attribute section depending on whether the fruit is round or long:
type
   // Declare a fruit record using case to choose the
   // diameter of a round fruit, or length and height ohterwise.
   TFruit = Record
     name : string[20];
     Case isRound : Boolean of // Choose how to map the next section
       True  :
         (diameter : Single);  // Maps to same storage as length
       False :
         (length   : Single;   // Maps to same storage as diameter
          width    : Single);
   end;
 
 
 var
   apple, banana : TFruit;
 
 
 begin
   // Set up the apple as round, with appropriate dimensions
   apple.name     := 'Apple';
   apple.isRound  := True;
   apple.diameter := 3.2;
 
 
   // Set up the banana as long, with appropriate dimensions
   banana.name    := 'Banana';
   banana.isRound := False;
   banana.length  := 7.65;
   banana.width   := 1.3;
  

   // Let us display the fruit dimensions:
   if apple.isRound
   then ShowMessageFmt('Apple diameter = %f',[apple.diameter])
   else ShowMessageFmt('Apple width = %f , length = %f',
                       [apple.width, apple.length]);
   if banana.isRound
   then ShowMessageFmt('Banana diameter = %f',[banana.diameter])
   else ShowMessageFmt('Banana width = %f , length = %f',
                       [banana.width, banana.length]);
 end;
Note that the Case statement now defines a variable, isRound to hold the type of the variant section. This is very useful, and recommended in variable length subsections, as seen in the code above.

0 التعليقات:

Post a Comment