Programmierkurs: Delphi: Pascal: Records

Was sind Records?
Bearbeiten

Records ermöglichen es, mehrere Variablen zu gruppieren. Dies ist beispielsweise dann hilfreich, wenn oft die gleiche Menge an Variablen benötigt wird, oder eine Menge Variablen logisch zusammengefasst werden soll. Eine weitere Situation in der Records unverzichtbar sind ist, wenn im Programm mehrere Datensätze gespeichert und verwaltet werden sollen, beispielsweise in einem Adressbuch.

Wie funktionieren Records?
Bearbeiten

Um zu unserem Beispiel vom Adressbuch zurückzukommen: Wir wollen alle Daten, also Vorname, Nachname, etc. in einem Record speichern. Dazu legen wir einen neuen Typ TPerson an, in dem wir alle Variablen auflisten:

type
  TPerson = record
    Vorname: string;
    Nachname: string;
    Anschrift: string;
    TelNr: string;
  end;


Wenn jetzt eine Variable vom Typ TPerson deklariert wird, enthält diese all diese Variablen:

var
  Person: TPerson;
begin
  Person.Vorname := 'Hans';
  Person.Nachname := 'Müller';
  { ... }
end;


Die Variablen im Record verhalten sich genauso wie „normale“ Variablen. Benötigt man ein Record nur zur einmaligen Strukturierung von Daten, ist es nicht nötig, einen Verbund-Typ anzulegen:

var
  Datei: record
    Name, Pfad: string;
  end;


Die with-Anweisung
Bearbeiten

Falls Sie mit mehreren Record-Feldern nacheinander arbeiten wollen, ist es sehr mühselig, immer den Namen der Variablen vornweg zu schreiben. Diese Aufrufe lassen sich mithilfe der with-Anweisung logisch gruppieren:

with Person do
begin
  Vorname := 'Hans';
  Nachname := 'Müller';
  Anschrift := 'Im Himmelsschloss 1, 12345 Wolkenstadt';
  TelNr := '03417/123456';
end;


Es ist auch möglich, die With-Anweisung auf mehrere Records anzuwenden. Dazu müssen die Bezeichner zwischen with und do mit Kommas getrennt aufgezählt werden. Natürlich müssen die Records zum gleichen Typ gehören.

Variante Teile in Records
Bearbeiten

Ein Record kann so genannte variante Teile enthalten. Dies sind Felder eines Records, die den gleichen Speicherplatz belegen, aber unterschiedliche Typen haben und/oder in verschiedener Anzahl vorhanden sind. Je nach verwendetem Bezeichner werden beim Schreiben und Lesen die Daten im Speicher entsprechend seines Typs anders interpretiert.

Deklaration
Bearbeiten

Der variante Teil eines Records ähnelt dabei einer case-Anweisung bei der verzweigten Datenverarbeitung (siehe Abschnitt „Verzweigungen“). Der variante Teil wird ebenfalls mit case eingeleitet und steht immer am Ende der Record-Deklaration. Ein solches Record ist daher so aufgebaut:

type
  Typname = record
    Feld_1: <Typ_1>;
    Feld_2: <Typ_2>;
    ...
    Feld_n: <Typ_n>;
    case [Markierungsname:] <Typ> of
      Wert_1: (<Feldliste_1>);
      Wert_2: (<Feldliste_2>);
      ...
      Wert_n: (<Feldliste_n>);
  end;


Von Feld_1 bis Feld_n erstreckt sich der statische Teil des Records wie in den oberen Abschnitten beschrieben. Vom Schlüsselwort case bis zum abschließenden end; folgt der variante Teil. Anders als bei den Verzweigungen schließt hierbei das end sowohl den varianten Teil als auch das gesamte Record ab. Daraus ergibt sich, dass die varianten Teile immer am Ende stehen und keine statischen Teile mehr folgen können.

Der Markierungsname muss hierbei nicht angegeben werden. Er dient zur Unterscheidung, welche der varianten Feldlisten die gültigen Daten enthält und kann daher wie die statischen Felder mit einem Wert belegt werden. Man sollte der Markierung immer einen eindeutigen Wert für die jeweilige Liste zuweisen, sobald man das Record mit Daten füllt. Beim Auslesen der Daten entscheidet dann der Wert der Markierung, welche varianten Felder gültige Daten enthalten und abgefragt werden dürfen. In manchen Fällen werden Sie vielleicht keine Markierung benötigen, Sie können dann den Bezeichner und den Doppelpunkt weglassen. Ein Typ und eine Wertliste muss aber in jedem Falle (sozusagen fiktiv) angegeben werden, wobei Sie jeden Aufzählungstyp verwenden können, also auch selbst definierte.

Die Feldlisten werden für jeden Wert von Klammern eingeschlossen. Diese Liste selbst unterscheidet sich nicht von anderen Felddeklarationen, sie entspricht also der Form:

 VarFeld_1: <Typ_1>;
 VarFeld_2: <Typ_2>;
 ...
 VarFeld_n: <Typ_n>;


Dabei müssen Sie jedoch beachten, dass kein Typ mit variabler Größe verwendet werden darf, da der variante Teil immer einen festen Speicherplatz belegt. Es ist daher kein string (mit Ausnahme von ShortString), dynamisches Array und keine Varianten (Datentyp Variant) erlaubt. Es darf auch kein Record verwendet werden, das einen solchen Typ enthält. Sie können stattdessen jedoch einen Zeiger auf solche Typen verwenden, da Zeiger immer eine feste Größe von 4 Byte haben.

Speicherbelegung
Bearbeiten

Die Größe des varianten Teils bestimmt sich nach der größten Feldliste. Um Anordnung und Größe der Daten zu verstehen, betrachten Sie bitte folgendes Beispiel:

type
  TVarRecord = record
    StatischesFeld1: Byte;
    StatischesFeld2: Boolean;
    case Byte of
      0:
        (VariantesFeld1: Byte;
         VariantesFeld2: Integer);
      1:
        (VariantesFeld3: array[1..10] of Char);
      2:
        (VariantesFeld4: Double;
         VariantesFeld5: Boolean);
  end;


Dieses Record wird im Speicher folgendermaßen abgelegt:

 

Falls Sie eine Markierung verwenden, wird diese zwischen dem letzten statischen und dem Beginn der varianten Feldliste gespeichert, das Record vergrößert sich entsprechend dem Typ der Markierung.

Wichtig ist für die Verwendung von varianten Records auch, dass zu jeder Zeit jedes der varianten Felder gelesen und geschrieben werden kann. Es kann daher immer nur eine der Feldlisten gültige Werte enthalten; Sie können also nicht die verschiedenen Werte gleichzeitig speichern. Sobald Sie im obigen Beispiel VariantesFeld3 mit Daten füllen, ändert sich automatisch der Wert aller anderen varianten Felder. Sie müssen daher besonders aufpassen, dass Sie keine benötigten Daten mit ungültigen überschreiben, weil Sie versehentlich einen falschen Feldnamen benutzen. Delphi wird Sie hiervor weder bei der Kompilierung noch bei der Ausführung Ihres Programms warnen!

Geben Sie zur Verdeutlichung einmal folgendes Programm ein und starten Sie es. Die Ausgabe wird Sie überraschen!

program VariantRecord;

{$APPTYPE CONSOLE}

type
  TVarRec = packed record
    case Byte of
      0:
        (FByte: Byte;
         FDouble: Double);
      1:
        (FStr: ShortString);
  end;

var
  rec: TVarRec;

begin
  rec.FByte := 6;
  rec.FDouble := 1.81630607010916E-0310;

  { Der Speicherbereich von FByte und FDouble
    wird jetzt als Zeichenkette interpretiert. }
  Writeln(rec.FStr);
  Readln;
end.


Hinweis: Das Schlüsselwort packed sorgt dafür, dass die Felder des Records lückenlos im Speicher aneinander gereiht und nicht für einen schnelleren Zugriff optimiert abgelegt werden.

Beispiel
Bearbeiten

In einem Adressbuch gibt es leider keine voneinander abhängigen Einträge, daher wenden wir uns wieder unserer Gastliste zu und erweitern diese.

Statt nur den Namen des Gastes zu speichern, wollen wir auch aufnehmen, ob der jeweilige Gast eingeladen ist, welche Gastnummer und welcher Platz ihm in diesem Falle zugewiesen wurde, oder ob sein Besuch andernfalls erwünscht ist. Wir können davon ausgehen, dass ein eingeladener Gast in jedem Fall erwünscht ist, benötigen hierzu also keine Information. Andererseits kann ein nicht eingeladener Gast keinen Platz und keine Gastnummer zugewiesen bekommen haben.

Diese Situation können wir in einem varianten Record darstellen:

type
  TGast = record
    Name, Vorname: string;  // bei statischen Feldern erlaubt
    case eingeladen: Boolean of
      True:
        (Platz, Gastnummer: Byte);
      False:
        (erwuenscht: Boolean);
  end;

  TGastListe = array of TGast;

var
  GastListe: TGastListe;
  Zaehler: Integer;
  Gast: TGast;


Name, Vorname und eingeladen sind bei jedem Gast vorhanden, die anderen Felder sollen (und dürfen) nur abhängig vom Wert des Feldes eingeladen verwendet werden. Nun können wir einige Gäste erfassen:

// Anzahl der Gäste festlegen
SetLength(GastListe, 4);

GastListe[0].Name := 'Schweiss';
GastListe[0].Vorname := 'Axel';
GastListe[0].eingeladen := False;
GastListe[0].erwuenscht := False;

GastListe[1].Name := 'Silie';
GastListe[1].Vorname := 'Peter';
GastListe[1].eingeladen := True;
GastListe[1].Platz := 42;
GastListe[1].Gastnummer := 1;

GastListe[2].Name := 'Pot';
GastListe[2].Vorname := 'Jack';
GastListe[2].eingeladen := False;
GastListe[2].erwuenscht := True;

GastListe[3].Name := 'Schluepfer';
GastListe[3].Vorname := 'Rosa';
GastListe[3].eingeladen := True;
GastListe[3].Platz := 14;
GastListe[3].Gastnummer := 2;


Anschließend wollen wir noch einen Türsteher beauftragen, die Meute am Eingang zu sortieren. Dazu schreiben wir in den Programmrumpf eine Funktion, die prüft, ob der Gast eingelassen werden kann (Hinweis: Zu der Verwendung von Funktionen, Schleifen und Verzweigungen erfahren Sie später mehr. Mit ein paar grundlegenden Englischkenntnissen lässt sich jedoch herausfinden, was die einzelnen Anweisungen bewirken.)

// Gast nur einlassen, wenn er eingeladen oder als nicht Eingeladener erwünscht ist
function GastEinlassen(AGast: TGast): Boolean;
begin
  if AGast.eingeladen then
    Result := True
  else
    Result := AGast.erwuenscht;
end;


Wenn der Gast eingeladen wurde, gibt diese Funktion True zurück, wenn er nicht eingeladen wurde, entsprechend, ob er erwünscht ist oder nicht.

Wenn Sie den Free Pascal Compiler verwenden, müssen Sie ihn mit fpc -Mdelphi aufrufen, oder {$MODE DELPHI} in die Quelldatei schreiben, damit die Pseudovariable Result erstellt wird, siehe Prozeduren und Funktionen.

Im Hauptprogramm läuft dann eine Schleife, die alle Gäste prüfen lässt und uns anzeigt, was unser Türsteher festgestellt hat.

// GastListe von Index 0 bis 3 durchlaufen
for Zaehler := 0 to 3 do
begin
  Gast := GastListe[Zaehler];
  Writeln('Name: ', Gast.Vorname, ' ', Gast.Name);
  if GastEinlassen(Gast) then
    Writeln('einlassen: ja')
  else
    Writeln('einlassen: nein');
  if Gast.eingeladen then
  begin
    Writeln('Gastnummer: ', Gast.Gastnummer);
    Writeln('Sitzplatz: ', Gast.Platz);
  end;
  Writeln;
end;


Vergessen Sie nicht, am Ende des Programms den vom dynamischen Array verwendeten Speicher wieder freizugeben, indem Sie SetLength(GastListe, 0); aufrufen.

Wenn Sie das Programm ausführen, werden Sie folgende Ausgabe erhalten:

Name: Axel Schweiss
einlassen: nein

Name: Peter Silie
einlassen: ja
Gastnummer: 1
Sitzplatz: 42

Name: Jack Pot
einlassen: ja

Name: Rosa Schluepfer
einlassen: ja
Gastnummer: 2
Sitzplatz: 14


Wie Sie sehen, erlaubt diese Art von Records größere Flexibilität für die Programmierung. Sie birgt jedoch auch die Gefahr schwer zu entdeckender Fehler, wenn man nicht sorgfältig genug programmiert.

Tricks mit varianten Records
Bearbeiten

Da bei varianten Records unterschiedliche Datentypen im selben Speicherbereich liegen, die je nach Zugriff anders interpretiert werden, kann man hiermit trickreiche und schnelle Datenumwandlungen durchführen. Man muss hierbei beachten, immer ein packed record zu verwenden. Bei nicht gepackten Records werden die Daten zur Erhöhung der Zugriffsgeschwindigkeit an Byte-Grenzen ausgerichtet. So können Lücken zwischen den Daten entstehen, die einer solchen Art der Datenumwandlung im Wege stehen.

Beispielsweise lässt sich mit einem varianten Record in einem Rutsch eine gesamte Zeichenkette quasi automatisch in die dezimalen ASCII-Werte umwandeln. Da keine tatsächliche Umwandlung erfolgt, sondern die Daten nur anders interpretiert werden, erfolgt dies zum „Nulltarif“, benötigt also keine Rechenzeit oder zusätzlichen Speicher. Es funktioniert wie folgt:

type
  TUmwandler = packed record
  case Byte of
    0: (Str: ShortString);
    1: (Bytes: array[0..255] of Byte);
  end;

var
  Umwandler: TUmwandler;
  i: Integer;

begin
  Umwandler.Str := 'Hallo Welt!';
  for i := 1 to Length(Umwandler.Str) do
    Writeln(Umwandler.Str[i], ' = ', Umwandler.Bytes[i]);
end.


Eine echte Zeichenkette als Feld des Records funktioniert hingegen nicht, da Zeichenketten vom Typ string intern nur Zeiger auf einen anderen Speicherbereich sind. Die Zählschleife beginnt bei 1 statt 0, da ShortString im Index 0 die Länge der Zeichenkette speichert.

Bei dem obigen Beispiel spart man sich die Umwandlung jedes einzelnen Zeichens mittels der Funktion Ord, bzw. in die andere Richtung mittels Chr.

Als zweites Beispiel kann man mit einem varianten Record auch prüfen, wie Daten im Arbeitsspeicher abgelegt werden. Wichtig für die Programmierung ist z.B. die so genannte Endianness, also die Reihenfolge, in der höher- und niederwertige Bytes abgelegt werden. Dazu kann man folgenden Trick verwenden:

type
  TEndianness = packed record
  case Byte of
    0: (IntWert: Integer);
    1: (Byte0, Byte1: Byte);
  end;

var
  Endian: TEndianness;

begin
  Endian.IntWert := $AFFE;  // höchstwertiges Byte steht links, also $AF
 
  case Endian.Byte0 of
    $AF: Writeln('Big-Endian');
    $FE: Writeln('Little-Endian');
  else
    Writeln('Endianness des Systems ist unbekannt.');
  end;
end.


Heutige PCs verwenden Little-Endian, das sollte also bei der Ausführung des Programms auch herauskommen. Byte0 entspricht dabei dem ersten im Speicher abgelegten Byte und Byte1 dem zweiten. Auch wenn unser Wert $AFFE ist, wird dieser im Arbeitsspeicher tatsächlich umgekehrt, also als $FEAF, abgelegt.

Sie werden eventuell bei Ihrer Programmiertätigkeit weitere Verwendungsmöglichkeiten hierfür entdecken. Diese Form der Datenumwandlung bzw. -auswertung ist sehr effizient. Dies betrifft sowohl Ausführungsgeschwindigkeit wie auch Speicherverbrauch. Zudem spart dies eine Menge Tipparbeit, die man sonst gegebenenfalls in die Programmierung mehr oder weniger umfangreicher Funktionen investieren musste.


  Pascal: Arrays Inhaltsverzeichnis Pascal: Varianten