Dieses Kapitel gehoert zum Kurs Programmieren in Oberon des Fachbereichs Informatik.

Partnerwahl fuer Anfaenger: Typen Bearbeiten

Nee, in diesem Kapitel geht es nicht um die Liebe. Liebe und Computer haben sehr wenig miteinander zu tun. Merkt euch das. Hier geht es vielmehr um Typen, was sind aber diese Dinger? Nehmen wir ein Beispiel: wir wollen eine Liste von Schülern verwalten. Jeder Schüler hat einen Namen (Vor- und Nachding), Noten und einen Durchschnitt (kommt irgendwie bekannt vor, nicht?). Anstatt jetzt mit all diesen Daten separat zu hantieren, können wir ihnen Namen und Form geben und vergessen, dass sie nur aus INTEGER und ARRAY OF CHAR besteht.

Die Übung dieses Kapitels besteht darin, ein Modul namens SchuelerListe zu schreiben, welche die Prozeduren Neu, Finde und Liste enthält, welche in etwa das tun, was ihre Namen versprechen.

Die Selbstbekenntnis Bearbeiten

Nun gut, oben wurde so schwammig angetönt, dass man mit Typen Variablen Name und Form geben kann. Wie geht das denn? Schauen wir uns doch die Deklaration eines Typen an

TYPE
    String = ARRAY 100 OF CHAR;

Auf die genaue Struktur und EBNF will ich nicht näher (was heisst da näher, ich hab's nicht einmal angedeutet!) eingehen, dies steht alles im offiziellen Oberon-Buch. Was uns beschäftigen soll ist die Bedeutung dieser Anweisung. Für den Compiler ist jetzt String ein ARRAY 100 OF CHAR. Eigentlich nix neues, das haben wir ja schon bei den Variablen gesehen. Interessant ist nun, das String gar keine Variable ist sondern eben ein Typ, aehnlich zum Beispiel INTEGER oder CHAR.

So wie wir es bis jetzt mit allen Typen gemacht haben, können wir eine Variable von unserem neuen Typ String deklarieren und zwar wie folgt:

VAR
    kleinerSatz : String;

Was ist jetzt so wahnsinnig gut daran? Naja, wir haben uns beim Tippen einige Zeichen gespart... Noch nicht allzu viel. Nun gut, spulen wir unsere Gehirne ein wenig zurück zum Kapitel über Arrays und Zeichenketten. Jedesmal wenn wir einer Prozedur ein Array übergeben, dürfen wir keine Länge festlegen, sowas in der Art

PROCEDURE LeseEingabe ( VAR ein : ARRAY OF CHAR ) : INTEGER;

Was ist nun, wenn wir nicht wollen, dass uns jemand ein zu kleines ARRAY liefert, und somit das ganze Programm zum Absturz reisst? Wir können ja nicht sagen, wir wollen ein ARRAY 100 OF CHAR, wir können aber sagen, er soll ein String bekommen. Das sieht dann wie folgt aus:

PROCEDURE LeseEingabe ( VAR ein : String ) : INTEGER;

Der Gag? Die Prozedur LeseEingabe erwartet dann bei jedem Aufruf eine Variable, welche als String deklariert wurde. Ist dies nicht der Fall, compiliert das Modul erst gar nicht. Der Programmierer kann also mit ruhigem Gewissen davon ausgehen, dass die Variable ein in LeseEingabe ein ARRAY 100 OF CHAR ist, kein CHAR grösser, kein CHAR kleiner.

Wie sieht es aber aus mit anderen Prozeduren? Die Prozedur Out.String erwartet ja als Eingabe ein ARRAY OF CHAR und doch kein String. Da aber String als ein ARRAY 100 OF CHAR typisiert wurde, geht dies natürlich auch:

Out.String(kleinerSatz);

Wir können auch, wie mit jedem normalen ARRAY die CHARs einzeln auslesen oder beschreiben, zum Beispiel

kleinerSatz[0] = 0X;

Selbstverständlich gilt das ganze nicht nur für ARRAY OF CHAR, sondern für jede erdenkliche Variable, die man sonst deklarieren kann.

Schizophrene Typen -- der RECORD Bearbeiten

Typen, wie wir sie bis jetzt kennen gelernt haben sind ganz schön nett und nützlich, werden mit der Zeit aber trotzdem etwas langweilig. Sie lösen (wenigstens) bis jetzt, nicht alle unsere Probleme.

In dieser Aufgabe wollen wir ja eine Liste von Schülern verwalten. Das einfachste, was einem dazu einfallen könnte, wäre, alle Daten in einem ARRAY zu schreiben und von dort aus darauf zuzugreifen. Jetzt haben wir ein kleines Problem: ein Schüler besteht aus mehr als einem Wert (geschweige denn auch aus Fleisch und Blut) und bis jetzt können wir nur ARRAYs von einzelnen Wertetypen schreiben. Wir koennten natuerlich mehrere ARRAYs deklarieren: eins fuer die Vornamen, eins fuer die Nachnamen, usw, aber das waere doch ein bisschen umstaendlich. Man tut viel besser dran, sowas zu schreiben:

TYPE
    Fach = RECORD
        bez : ARRAY 4 OF CHAR;
        note : INTEGER;
        gewicht : INTEGER;
    END;
    Schueler = RECORD
        name, vorname : ARRAY 20 OF CHAR;
        faecher : ARRAY 10 OF Fach;
        schnitt : INTEGER;
    END;

Vielleicht ging das ein wenig zu schnell... Wir können auch Typen definieren, welche mehrere Variablen enthalten. Sie fangen mit dem Wort RECORD an, listen die Variablen auf, wie bei jeder normalen Variablendeklaration, und finden beim wort END ihr Ende. Die Variablen im RECORD können wir als ihre "Felder" anschauen.

Der Zugriff auf die Variablen geht ganz einfach vor sich. Zuerst müssen wir einen Schüler haben, also deklarieren wir einen:

VAR
    maxMuster : Schueler;

Jetzt wo er deklariert ist, können wir seine Werte setzen, und zwar indem wir den Namen der Variablen hinzuschreiben, einen Punkt und schlussendlich den Namen des Feldes, dass wir beschreiben wollen und dies als Variable benützen. Tönt kompliziert? Nanu, in der Praxis sieht es dann so aus:

maxMuster.name := "Muster";
maxMuster.vorname := "Max";
maxMuster.faecher[0].bez := "INF"
maxMuster.faecher[0].note := 6;
maxMuster.faecher[0].gewicht := 3;

Also, wir haben einen Schueler namens Max Muster kreiert, welcher im Fach Informatik, welches dreifach zählt, eine 6 bekommen hat. Wie aus dem Beispiel ersichtlich, können wir diese RECORDS in ARRAYs stopfen oder ineinander verschachteln, ohne dass uns was Schlimmes widerfaehrt.

Vielleicht noch ein kleines Detail: das Feld bez im Typus Fach wurden als ARRAY 4 OF CHAR definiert, obwohl das Fach nur drei Zeichen lang ist; wir brauchen ja noch Platz für das 0X am Schluss.

Jetzt spielen wir noch ein wenig damit rum... Bearbeiten

Gut, wir haben so einen Schueler definiert und mit Daten aufgefüllt. Was kann man noch damit machen?

schuelerEins := schuelerZwei;

Sind die beiden Variablen schuelerEins und schuelerZwei vom gleichen Typ, so ist diese Anweisung zulässig. Alle Daten vom RECORD schuelerZwei werden nach schuelerEins kopiert, auch alle uninitialisierten Felder!

IF schuelerEins = schuelerZwei THEN...

hingegen ist nicht zulässig. Will man zwei RECORDs miteinander vergleichen, so muss man dies von Hand erledigen. Auch die Operatoren <, >, # und dergleichen können mit RECORDs nix anfangen.

"Son of a Bitch" -- die Vererbung Bearbeiten

Obwohl Computer nicht allzu biologisch veranlagt sind, kennen sie sowas wie die Vererbung. Das ganze erklärt man am besten von Vorne herein mit einem kleinen Beispiel

TYPE
    Fach = RECORD
        bez : ARRAY 4 OF CHAR;
        note : INTEGER;
        wert : INTEGER;
    END;
    Schueler = RECORD
        name, vorname : ARRAY 20 OF CHAR;
        faecher : ARRAY 10 OF Fach;
        schnitt : INTEGER;
    END;
    Schueler_mit_adr = RECORD ( Schueler )
        strasse, ort : Array 20 OF CHAR
        plz : ARRAY 5 OF CHAR
    END;

Also, wir haben noch unseren Schueler aus dem ersten Beispiel und wollen jetzt einen neuen Typus kreieren, welcher die genau gleichen Daten wie Schueler enthält, plus einige Felder für dessen Adresse. Anstatt alles von Vorne wieder zu definieren, sagen wir dem Compiler, für den Typus Schueler_mit_adr soll er ein RECORD kreieren, welches den Typus Schueler um die Felder strasse, ort und plz erweitert.

Die Verschachtelung solcher Vererbungen kann beliebig tief erfolgen, man kann jedoch keine Typen paaren, sprich von zwei verschiedenen Typen erben lassen. Liebe und Computer haben sehr wenig miteinander zu tun. Merkt euch das. Hat man nun zwei Typen definiert, welche ein Mutter-Kind Verhältnis zueinander pflegen, etwa zum Beispiel so,

TYPE
    Mutter = RECORD
        data1 : INTEGER;
    END;
    Kind = RECORD ( Mutter )
        data2 : INTEGER;
    END;

So können einige Anweisungen typenübergreifend ausgeführt werden. Nehmen wir zuerst die Anweisung

mutter := kind;

wobei die Variablen mutter und kind als ihre entsprechende Typen deklariert sind. Die Anweisung ist zulässig und schreibt die Daten von kind.data1 in mutter.data1. Das Feld data2 in kind wird ignoriert. Es werden also nur die Felder kopiert, welche in beiden Typen vorhanden sind. Probiert man jedoch das umgekehrte

kind := mutter;

kommt man nicht allzu weit, denn der Compiler zieht einem schon die Bremse. Da kind mehr Daten braucht als diejenige, die in mutter enthalten sind, kann keine Zuweisung erfolgen.

Etwas ähnliches geschieht bei der Variablenübergabe bei Prozeduren. Nehmen wir an wir hätten folgende Prozedur:

PROCEDURE Test1 ( k : Kind );

Versucht man nun folgendes

Test1(mutter);

kommt sich der Compiler irgendwie verarscht vor und meldet einen Fehler. Die Umkehrung, also

PROCEDURE Test2 ( m : Mutter);

erlaubt jedoch folgende Anweisung

Test2(kind);

Als Faustregel bei Zuweisungen und Parameterübergabe kann man sich also folgendes merken: sind zu viele Daten vorhanden, ist alles gut, sind zu wenige da, compiliert das Zeugs erst gar nicht.

"'Good God', said God, 'I've got my work cut out.'[1]" Bearbeiten

Also nun, jetzt geht's an die Arbeit. Die Deklaration vom Typus Schueler haben wir schon oben unternommen und können es da einfach abschreiben, wir verzichten jedoch auf die Noten und Fächer, nehmen jedoch die Legi-Nummer hinzu.

Interessant wird nur noch die Verwaltung des ARRAYs mit den Schülernamen. Wir setzen mal eine Grösse von 100 Einträge als Maximum, welche bei der Position 0 im ARRAY anfangen und von dort aus abgelegt werden. Die Anzahl Elemente, welche schon abgespeichert wurden, speichern wir in einem INTEGER, welches uns auch noch die nächste freie Zelle zum Eintragen eines neuen Schuelers liefert. Die Prozedur GetString ist einfach die Implementierung dessen, was wir im Kapitel über Zeichenketten besprochen haben. Die kleine Hilfsprozedur PrintSchueler kriegt einen Schueler als Argument und erledigt dessen Ausgabe, ähnlich wie Out.String für Zeichenketten. Somit sparen wir uns einen Haufen Arbeit. Die übrigen Prozeduren sind nicht so spannend und dienen lediglich als Beispiel für den Umgang mit Typen.

MODULE SchuelerListe;

    IMPORT
        In, Out;

    TYPE
        String = ARRAY 100 OF CHAR;
        Schueler = RECORD
            name, vorname : String;
            legi : LONGINT;
        END;

    VAR
        liste : ARRAY 100 OF Schueler;
        anzSchueler : INTEGER;

    PROCEDURE GetString ( VAR s : String );
        VAR
            c : CHAR;
            pos : INTEGER;
        BEGIN
            pos := 0;
            In.Char(c);
            WHILE c = " " DO
                In.Char(c);
            END;
            WHILE c # " " DO
                s[pos] := c;
                INC(pos);
                In.Char(c);
            END;
            s[pos] := 0X;
        END GetString;

    PROCEDURE PrintSchueler ( s : Schueler );
        BEGIN
            Out.String(s.name); Out.Char(9X); (* tabulator *)
            Out.String(s.vorname); Out.Char(9X);
            Out.Int(s.legi,10); Out.Ln;
        END PrintSchueler;

    PROCEDURE Neu*;
        BEGIN
            In.Open;
            getString(liste[anzSchueler].name);
            getString(liste[anzSchueler].vorname);
            In.LongInt(liste[anzSchueler].legi);
            INC(anzSchueler);
        END Neu;

    PROCEDURE Finde*;
        VAR
            i : INTEGER;
            legi : LONGINT;
        BEGIN
            In.Open;
            In.LongInt(legi);
            i := 0;
            WHILE i < anzSchueler DO
                IF legi = liste[i].legi THEN
                    printSchueler(liste[i]);
                    RETURN;
                END;
                INC(i);
            END;
            Out.String("Schueler nicht gefunden!"); Out.Ln;
        END Finde;

    PROCEDURE Liste*;
        VAR
            i : INTEGER;
        BEGIN
            FOR i := 0 TO anzSchueler - 1 DO
                PrintSchueler(liste[i]);
            END;
        END Liste;

    BEGIN
        anzSchueler := 0;
        Out.Open;
    END SchuelerListe.

Fussnoten Bearbeiten

  1. Aus Epigrams, no. 1, "The Dilemma" von J.C. Squire (1882-1958).