GNU-Pascal in Beispielen: Typen im Eigenbau

zurück zu GNU-Pascal in Beispielen

Typen im EigenbauBearbeiten

Die grundlegenden Typen wurden bereits im Kapitel Variablen und Typen besprochen. Dieses Kapitel beschäftigt sich damit, auf der Basis dieser grundlegenden Typen eigene Typen zu entwickeln.

Üblicherweise wird selbstdefinierten Typen immer ein "T" vorangestellt.

Eigene StringtypenBearbeiten

Grundsätzlich ist an dieser Stelle bekannt, wie Strings mit einer bestimmten Kapazität erzeugt werden. Statt aber, wie in Kapitel Zeichenketten alle Strings mit ihrer Kapazität anzugeben, ist es viel bequemer, sich eigene Stringtypen selbst zu erzeugen:

Program: StringtypBearbeiten

program Stringtyp;

const
  Kapazitaet = 20;

type
  TMeinString = String (Kapazitaet);

var
  MeinString: TMeinString;

begin
  MeinString := 'Hallo, Welt!';
  WriteLn (MeinString)
end.

ErklärungBearbeiten

Im Bereich von type werden eigene Typen definiert. TMeinString ist damit ab sofort ein Synonym für String (20). Dieser Typ kann im var-Bereich sofort genutzt werden.

Die hier gezeigte Reihenfolge von const, type und var ist die natürlichste Art der Anordnung. Wenn keine Gründe gegen diese Reihenfolge sprechen, so sollte sie beibehalten werden.

Eigene ganzzahlige TypenBearbeiten

Manchmal ist es nützlich, eigene ganzzahlige Typen zu definieren. Das folgende Programm demonstriert die Vorgehensweise:

Program: IntegertypBearbeiten

program Integertyp;

type
  TMeinInteger = Integer (3);
  TMeinCardinal = Cardinal (3);

begin
  WriteLn ('Kleinster TMeinInteger Wert:  ', Low (TMeinInteger));
  WriteLn ('Groesster TMeinInteger Wert:  ', High (TMeinInteger));
  WriteLn ('Kleinster TMeinCardinal Wert: ', Low (TMeinCardinal));
  WriteLn ('Groesster TMeinCardinal Wert: ', High (TMeinCardinal))
end.

ErklärungBearbeiten

Es werden zwei verschiedene ganzzahlige Typen definiert, wobei einer vorzeichenbehaftet, der Andere vorzeichenlos ist. Beide haben eine Kapazität von drei Bits. Die Funktion Low liefert den kleinsten Wert, den eine Variable von diesem Typ annehmen kann, die Funktion High den Höchsten.

RecordsBearbeiten

Records sind aus beliebigen Typen in beliebiger Anzahl zusammengesetzte Typen. Ein Record könnte so beispielsweise alle Anschriftendaten über Personen zusammenfassen, geometrische Figuren organisieren oder die Informationen einer /etc/passwd-Datenzeile gruppieren.

Arbeiten mit RecordsBearbeiten

Ein Record wird definiert, indem alle "Record-Felder" aufgeführt werden:

Program: Record1aBearbeiten
program Record1a;

type
  TEintrag = String (100);
  TPostleitzahl = String (5);

  TMitarbeiter = record
    PersonalNummer: Integer;
    Vorname, Nachname, Strasse, Stadt: TEintrag;
    PLZ: TPostleitzahl
  end;

var
  Person: TMitarbeiter;

begin
  Person.PersonalNummer := 7;
  Person.Vorname        := 'Hans';
  Person.Nachname       := 'Dampf';
  Person.Strasse        := 'In den Gassen 1-100';
  Person.Stadt          := 'Ueberall';
  Person.PLZ            := '12345'
end.
ErklärungBearbeiten

Gefolgt von dem Schlüsselwort record werden die einzelnen Felder wie bei der Variablendeklaration aufgeführt. Eine Initialisierung während der Typendefinition ist nicht möglich. Der Record endet mit dem abschließenden end. Auf die einzelnen Felder eines Records wird mit Hilfe der Punkt-Schreibweise zugegriffen. So meint Person.Stadt das "Stadt-Feld" des Records.

Records können bei der Variablendeklaration initialisiert werden, wie folgendes Beispiel zeigt:

Program: Record2Bearbeiten
program Record2;

type
  TPunkt = record
    X, Y: Integer
  end;
  TKreis = record
    Mitte: TPunkt;
    Radius: Real
  end;

var
  Ort: TPunkt = (100, 50);
  Kreis: TKreis = ((19, 23), 30.1);

begin
  WriteLn ('Ort = (', Ort.X, ';', Ort.Y, ')');
  WriteLn ('Kreis = (', Kreis.Mitte.X, ';', Kreis.Mitte.Y,') Radius = ',
   Kreis.Radius : 0 : 2)
end.
ErklärungBearbeiten

Es werden zwei Record-Typen erzeugt, TPunkt und TKreis, wobei TKreis TPunkt als Feld Mitte enthält. Die Variablendeklaration Ort initialisiert die zwei Feldvariablen mit Hilfe von (100, 50), sodass Ort.X = 100 und Ort.Y = 50 ist.

Kreis hingegen wird so initialisiert, dass Kreis.Mitte wie Ort initialisiert wird, diesmal allerdings zu (19, 23). Die Feldvariable Radius wird mit der Zahl 30.1 initialisiert. Beachten Sie dabei die geschachtelte Klammerung.

Der Zugriff auf die Record-Felder erfolgt wieder in der Punkt-Schreibweise. Ort.X ist die X-Koordinate des Punktes, Kreis.Radius der Radius des Kreises. Der Mittelpunkt des Kreises ist Kreis.Mitte, seine X-Koordinate lautet Kreis.Mitte.X.

Ein häufiges Einsatzgebiet von Records ist das Speichern von Datensätzen innerhalb von Arrays. Folgendes Beispiel demonstriert das Vorgehen:

Program: Record3Bearbeiten
program Record3;

const
  IndexEnde = 10;

type
  TPunkt = record
    X, Y: Integer
  end;
  
  TPunkte = array [1..IndexEnde] of TPunkt;

var
  Orte: TPunkte;
  Index: Integer;

begin
  for Index := 1 to IndexEnde do
    begin
      Orte[Index].X := Index;
      Orte[Index].Y := Index * 2
    end;
  for Index := 1 to IndexEnde do
    WriteLn ('Orte[', Index, '] = (',
      Orte[Index].X, ';', Orte[Index].Y, ')')
end.
ErklärungBearbeiten

Die Deklaration des Arrays erfolgt wie bei elementaren Typen, lediglich die Initialisierung ist verschieden: Auf die einzelnen Record-Felder wird gesondert zugegriffen.


WithBearbeiten

Der Zugriff auf die einzelnen Komponenten des Arrays aus Beispiel Record1 ist schon bei wenigen Feldern sehr mühsam zu schreiben. Jedes Mal musste "Person." vor das gemeinte Feld notiert werden. Eine abkürzende Schreibweise auf Record-Komponenten demonstriert folgendes Beispiel:

Program: Record1bBearbeiten
program Record1b;

type
  TEintrag = String (100);
  TPostleitzahl = String (5);
  
  TMitarbeiter = record
    PersonalNummer: Integer;
    Vorname, Nachname, Strasse, Stadt: TEintrag;
    PLZ: TPostleitzahl
  end;

var
  Person: TMitarbeiter;

begin
  with Person do
    begin
      PersonalNummer := 7;
      Vorname        := 'Hans';
      Nachname       := 'Dampf';
      Strasse        := 'In den Gassen 1-100';
      Stadt          := 'Ueberall';
      PLZ            := '12345'
    end
end.
ErklärungBearbeiten

Die abkürzende Schreibweise mit with erspart das mehrfache Schreiben von "Person.". Trotz dieser Struktur darf innerhalb des with-Blockes weiterhin Person.Nachname geschrieben werden, wenn es der Umstand erfordert. With-Blöcke dürfen beliebig geschachtelt werden, wobei darauf zu achten ist, dass nicht mehrere Record-Variablen die gleichen Feldbezeichner haben. Fehler, die daraus resultieren, sind schwer zu finden.

Variante RecordsBearbeiten

Erweiterten wir die Menge der geometrischen Objekte aus Record2 um einige weitere Typen, so würden wir schnell feststellen, dass sie gemeinsame Eigenschaften haben die es nahelegen, alle diese Typen in einem gemeinsamen Record zu vereinigen. Eine Möglichkeit könnte darin bestehen, alle diese Typen einzeln in das Record aufzunehmen, wie folgendes Beispiel zeigt:

Program: Variant1Bearbeiten
program Variant1;

type
  TObjektTyp = (Punkt, Kreis, Rechteck);

  TPunkt = record
    X, Y: Integer
  end;

  TKreis = record
    X, Y: Integer;
    Radius: Real
  end;

  TRechteck = record
    X, Y, Breite, Hoehe: Integer
  end;

  TGeometrie = record
    Typ: TObjektTyp;
    Punkt: TPunkt;
    Kreis: TKreis;
    Rechteck: TRechteck
  end;

begin
  WriteLn ('Größe TObjektTyp = ', SizeOf (TObjektTyp));
  WriteLn ('Größe TPunkt     = ', SizeOf (TPunkt));
  WriteLn ('Größe TKreis     = ', SizeOf (TKreis));
  WriteLn ('Größe TRechteck  = ', SizeOf (TRechteck));
  WriteLn ('Größe TGeometrie = ', SizeOf (TGeometrie))
end.
ErklärungBearbeiten

In diesem Beispiel werden alle Typen in einem Record zusammengefasst. die Funktion SizeOf liefert uns die Größe eines Objektes in Bytes. Bei diesem Beispiel stellt sich der Nachteil ein, dass TGeometrie die gleiche Größe hat, wie die Summe der einzelnen Typen.

Eine wesentlich elegantere Art, TGeometrie zu definieren ist folgende:

Program: Variant2Bearbeiten
program Variant2;

type
  TObjektTyp = (Punkt, Kreis, Rechteck);

  TGeometrie = record
    X, Y: Integer;     { gemeinsam für alle Geometrie-Typen }
    case Typ: TObjektTyp of
      Kreis:    (Radius: Real);
      Rechteck: (Breite, Hoehe: Integer)
  end;

var
  MeinKreis, MeinPunkt: TGeometrie;

begin
  WriteLn ('Größe TGeometrie = ', SizeOf (TGeometrie));

  with MeinKreis do
    begin
      Typ := Kreis;
      X := 10;
      Y := 10;
      Radius := 3.0
    end;

  with MeinKreis do
    WriteLn ('(', X, ';', Y, ') R= ', Radius);

  with MeinPunkt do
    begin
      Typ := Punkt;
      X := 20;
      Y := 20
    end;

  with MeinPunkt do
    WriteLn ('(', X, ';', Y, ')')
end.
ErklärungBearbeiten

Diejenigen Record-Felder, die allen Geometrietypen gemeinsam waren, wurden in der neuen Definition von TGeometrie an den Anfang des Records gestellt. Danach folgt eine Auswahl der Record-Felder mittels case Typ: TObjektTyp of. Das Record-Feld Typ wird tatsächlich zum Bestandteil des Records, diese Art der Darstellung erlaubt es, aus der Menge von Varianten Kreis und Rechteck beschreibend auszuwählen. Wenn Typ = Kreis ist, so steht uns das Record-Feld Radius zur Verfügung, ist Typ = Rechteck, dann sind Breite und Hoehe verfügbar [1].

Der Fall Typ = Punkt braucht nicht gesondert abgefangen zu werden, seine Beschreibung liegt außerhalb der Varianten im oberen Bereich des Records. Die Größe des jetzigen TGeometry-Typs ist nur noch das Maximum der Größe der Varianten plus der Größe der außerhalb des Varianten-Teils liegenden Record-Felder. Eine Variable vom Typ TGeometry benötigt mit dieser Methode ungefähr die Hälfte des Speichers, den eine entsprechende Variable aus Variant1 belegen würde.

ZeigerBearbeiten

Alle bisher beschriebenen Variablendeklarationen gingen davon aus, dass wir eine zum Zeitpunkt des Programmierens bekannte Anzahl von Datenelementen speichern wollen. Ist die Anzahl der Elemente, die wir speichern wollen aber beliebig und die Typen vielleicht unterschiedlich, so helfen Zeiger dabei, eine dynamische Speicherverwaltung zu erzeugen. Ein Zeiger ist lediglich ein Verweis auf die Speicherstelle einer Variablen [2]. Ein einführendes Beispiel mit Zeigern zeigt folgendes Listing:

Program: ZeigerBearbeiten

program Zeiger;

type
  PInteger = ^Integer;

var
  RefZahl: PInteger = nil;

begin
  New (RefZahl);
  RefZahl^ := 42;
  WriteLn ('Inhalt  = ', RefZahl^);
  WriteLn ('Adresse = ', Cardinal (RefZahl));
  Dispose (RefZahl)
end.

ErklärungBearbeiten

Ein Zeigertyp wird definiert, indem ein Dach "^" vor den Grundtyp gestellt wird. Das Bedeutet in obigem Fall "Zeiger auf Integer". Allgemeine Zeiger auf nicht näher spezifizierte Typen sind Pointer.

Deklariert werden Variablen von Zeigertypen genauso wie von allen anderen Typen, neu ist die Initialisierung mit dem Schlüsselwort nil. Es sollte hier am Anfang des Kapitels nur der Vollständigkeit aufgeführt werden und bedeutet einen Zeiger auf "nichts", also einen leeren Zeiger [3]. Die Prozedur New erzeugt den Speicherplatz, auf dem in Zukunft ein Wert vom Typ Integer liegen soll. Dieser Wert kann zugewiesen werden, wobei das Dach diesmal nachgestellt wird. Diese Art des Zugriffs nennt man dereferenzieren. Auf die gleiche Weise kann der Inhalt des Zeigers ausgegeben werden.

Der Zeiger selbst kann mit Hilfe eines Casts ausgegeben werden, wobei hier die Speicheradresse ermittelt [4] wird, an der die Zahl 42 abgelegt wurde [5].

Dispose gibt den Speicherplatz, der mit New erzeugt wurde, wieder frei. Obwohl der Speicher freigegeben wurde, verweist der Zeiger möglicherweise noch an die Position, wo die Zahl 42 abgelegt wurde. Verlassen sollte man sich darauf auf keinen Fall, denn ein freigewordener Speicher kann jeden beliebigen Wert haben, auch den ursprünglichen. Erst beim erneuten Überschreiben der Speicheradresse durch das Betriebssystem ändert sich dieser Wert.

Zeigertypen sollte, so lange nichts dagegen spricht, immer ein "P" vorrangestellt werden.

Ein weitaus komplexeres Beispiel der Speicherverwaltung stellen so genannte Stapel dar. Ein Stapel ist eine Datenstruktur, bei der neu eingefügte Daten über [6] alle anderen Daten gestellt werden. Diejenigen Daten, die zuerst eingegeben wurde sind die Untersten des Stapels. Schaut man sich die einzelnen Elemente des Stapels an, so beginnt man von oben, also in umgekehrter Reihenfolge der Eingabe der Daten. Das folgende Programm liest einen "Stapel Namen" ein, gibt ihn aus und löscht ihn anschließend wieder:

Program: Stapel1Bearbeiten

program Stapel1;

type
  TNamenString = String(100);
  PNamen = ^TNamen;
  TNamen = record
    Name: TNamenString;
    Naechster: PNamen
  end;

var
  NamenStapel, TempName: PNamen = nil;
  Abbruch: Boolean = False;
  Name: TNamenString;
  Nummer: Integer;

begin
  Write ('Erzeugt eine Namensliste. ');
  WriteLn ('Abbruch durch leere Zeile');
  { Namensstapel aufbauen }
  repeat
    Write ('Geben Sie einen Namen ein: ');
    ReadLn (Name);
    if Length (Name) = 0 then
      Abbruch := True
    else
      begin
        { neuen Speicherplatz reservieren }
        New (TempName);
        { Namen eintragen }
        TempName^.Name := Name;
        { alten Stapel unter den Neuen legen }
        TempName^.Naechster := NamenStapel;
        {  Alter Stapel ist neuer Stapel }
        NamenStapel := TempName
      end
  until Abbruch;

  TempName := NamenStapel;  { oberstes Stapelelement }
  Nummer := 1;

  { Namensstapel ausgeben }
  while TempName <> nil do
    begin
      WriteLn (Nummer, 'ter Name: ', TempName^.Name);
      Inc (Nummer);
      TempName := TempName^.Naechster
    end;
 
  { Namensstapel loeschen }
  TempName := NamenStapel;  { oberstes Stapelelement }
  while TempName <> nil do
    begin
      { Namenstapel zeigt auf zweites Element }
      NamenStapel := NamenStapel^.Naechster;
      WriteLn ('entferne ', TempName^.Name);
      Dispose (TempName);      { Speicher wieder freigeben }
      TempName := NamenStapel  { oberstes Stapelelement }
    end
end.

ErklärungBearbeiten

Im Typenbereich werden drei Typen deklariert, einer für Strings mit einer festen Größe (TNamenString), ein Zeiger auf einen Record (PNamen) und der Record selber (TNamen). Auffällig ist, dass der Zeigertyp (PNamen) vor dem Typen (TNamen), dessen Verweis er ist, deklariert wurde. Dies ist erlaubt und an dieser Stelle auch durchaus erwünscht, denn innerhalb der Definition des Records TNamen wird dieser Zeigertyp als Typ des Record-Feldes Naechster eingesetzt. Verweist ein Record-Feld auf diese Weise auf sich selbst, so spricht man von einer "rekursiven Datenstruktur".

Der Namensstapel wird wie folgt aufgebaut: Wurde ein "richtiger Name" eingegeben, so wird mit Hilfe der Prozedur New ein Speicherplatz für TNamen erzeugt. Der Name wird dem Record-Feld Name zugewiesen. Dabei wird mit Hilfe des Daches der Zeiger dereferenziert, so dass eine Variable vom Typ TNamen vorliegt.

Auf die Record-Felder dieser Variablen greift man mit der bekannten "Punkt-Schreibweise" zu. Das Feld Naechster wird mit dem Zeiger auf den alten Stapel belegt, wobei dieser "alte Stapel" beim ersten Durchlauf nil ist. Der Zeiger Namensstapel, welcher soeben noch einen Zeiger auf das erste Element des restlichen Stapels war, wird nun zu einem Zeiger auf das oberste Element des Stapels. Auf diese Weise wird fortwährend der alte Stapel unter das neu erzeugte Element TempName gelegt.

Die Daten des Namensstapels werden ausgegeben, indem der Stapel von "oben nach unten" durchlaufen wird. Am Anfang zeigt TempName auf das oberste Element des Stapels und wird bei jedem Durchlauf auf seinen Nachfolger gesetzt, die Anzahl der Stapelelemente wird zwecks Anzeige mitprotokolliert. Beachten Sie, dass die Reihenfolge der Ausgabe in umgekehrter Reihenfolge der Eingabe erfolgt. Speicher, der dynamisch alloziert [7] wurde, muss spätestens zum Ende des Programms wieder freigegeben werden. TempName verweist zu Beginn auf das oberste Stapelelement. Solange der Stapel noch nicht vollständig abgebaut wurde, wird die Variable NamenStapel auf das Element hinter TempName gesetzt. Damit enthält NamenStapel wieder den "Rest" des Stapels. Der Speicher des obersten Elementes wird mit Dispose entfernt. Dieser nun freigewordene Zeiger kann anschließend wieder auf das erste Element des Stapels gesetzt werden und die Schleife kann erneut durchlaufen werden.


SchemataBearbeiten

Schemata dienen ebenso wie Zeiger dazu, Speicher dynamisch zu verwalten. Im Gegensatz zu Zeigern muss die maximale Anzahl der Elemente, die gespeichert werden sollen, bekannt sein, wobei diese Anzahl erst zur Laufzeit feststehen muss. Einen Schematypen kennen Sie bereits, es handelt sich dabei um Strings. Schemata und Zeiger lassen sich auch kombinieren. Das einführende Beispiel zeigt den grundsätzlichen Umgang mit Schemata:

Program: Schema1Bearbeiten

program Schema1;

type
  TSchemaType (Anz: Byte) = array [1..Anz] of Integer;

var
  MeinSchema: TSchemaType (4);
  i: Integer;

begin
  WriteLn ('MeinSchema hat maximal ', MeinSchema.Anz, ' Elemente.');
  for i := 1 to MeinSchema.Anz do
    MeinSchema[i] := 5 * i;
  for i := 1 to MeinSchema.Anz do
    WriteLn (MeinSchema[i])
end.

ErklärungBearbeiten

Schematypen werden definiert, indem mindestens eine Variable, in unserem Beispiel Anz, dem Typenname folgt. Diese Variable, die auch als Diskiminante bezeichnet wird, wird als eine der Grenzen eines Arrays [8] benutzt. Das Deklarieren eines Schemas erfolgt analog zum Deklarieren eines Strings.

Die maximale Anzahl der Elemente wird angegeben, was in unserem Fall bedeutet, dass MeinSchema ein Array von vier Integer-Variablen ist. Die Anzahl der Elemente lässt sich nachträglich während des Programmlaufes herausfinden, indem das zum Schema passende Feld Anz in der bekannten Weise ausgelesen wird. Auf die Variable MeinSchema kann zugegriffen werden, wie auf ein Array.

Schemata dienen der Bequemlichkeit. Benötigt man mehrere Variablen vom gleichen Array-Typ, jedoch mit möglicherweise unterschiedlicher Anzahl von Variablen, so sind sie genau die richtige Wahl.

mehrdimensionale Arrays zu erzeugen oder den Anfang und das Ende des Arrays festzulegen, wie folgendes Beispiel zeigt:

Program: Schema2Bearbeiten

program Schema2;

type
  TSchemaType (Anfang, Ende: Cardinal) =
    array [Anfang..Ende] of Integer;

var
  MeinSchema: TSchemaType (1, 4) = (5, 6, 7, 8);
  i: Integer;

begin
  for i := MeinSchema.Anfang to MeinSchema.Ende do
    WriteLn (MeinSchema[i])
end.

ErklärungBearbeiten

In diesem Beispiel werden mit Hilfe der beiden Diskriminanten Anfang und Ende die Grenzen des Arrays festgelegt. Diese Grenzen müssen bei der Deklaration der Variablen angegeben werden. Wie auf diese Diskriminanten zugegriffen wird, zeigt die for-Schleife.

Schemata können mit Zeigern kombiniert werden und zur Laufzeit dynamisch erzeugt werden. Das folgende Programm ist eine Abwandlung von Schema2 welches diese Fähigkeit demonstriert:

Program: Schema3Bearbeiten

program Schema3;

type
  PSchemaType = ^TSchemaType;
  TSchemaType (Anfang, Ende: Cardinal) = array [Anfang..Ende] of Integer;

var
  MeinSchema: PSchemaType;
  i: Integer;

begin
  New (MeinSchema, 4, 6);
  for i := MeinSchema^.Anfang to MeinSchema^.Ende do
    MeinSchema^[i] :=  10 * i;
  for i := MeinSchema^.Anfang to MeinSchema^.Ende do
    WriteLn (MeinSchema^[i]);
  Dispose (MeinSchema)
end.

ErklärungBearbeiten

Es wird ein Zeiger auf den Schematyp definiert, der genutzt wird, um MeinSchema zu deklarieren. Der Speicher für MeinSchema wird dynamisch angefordert, wobei der Prozedur New beide Grenzen des zugehörigen Arrays übergeben werden. Auf Anfang und Ende wird mit der bekannten Zeiger-Schreibweise zugegriffen, als sei MeinSchema ein Record [9]. Das Array erhält man, indem der Schemazeiger dereferenziert wird. Auf die einzelnen Variablen kann sodann mit der Index-Schreibweise zugegriffen werden.


AnmerkungenBearbeiten

  1. Aktuell ist es prinzipiell möglich, alle verfügbaren Record-Felder zu überschreiben, unabhängig davon, wie Typ belegt wurde. Hierauf muss bei der Programmierung selbst geachtet werden.
  2. So wie die Adresse einer Person nicht die Person selber bedeutet sondern einen Verweis auf sie.
  3. Stellen Sie sich ruhig einen Zeiger auf "nichts" wie eine leere, noch nicht ausgefüllte Anschrift vor.
  4. An die wir einen Brief schicken könnten mit der Frage nach ihrem Inhalt.
  5. Die wir mit Hilfe der verschachtelten Typencasts
    Integer (Pointer (Cardinal (RefZahl))^)
    
    wieder erhalten könnten.
  6. Wie bei einem Stapel Teller: Der zuletzt auf den Stapel gelegte Teller ist der Oberste.
  7. So nennt man das Zuteilen von Speicher, zum Beispiel mit der Prozedur New.
  8. Schemata müssen nicht, so wie in unseren Beispielen, auf Arrays basieren, aber es ist vermutlich das häufigste Einsatzgebiet.
  9. Tatsächlich sind Schemata wie Records implementiert.