Home » Tutorials » Object Pascal/RTL » Delphi-Crashkurs

Delphi-Crashkurs

Objektorientierte Programmierung

Delphi Language war früher „Object Pascal„. Dieser Name impliziert eine Funktionalität, welche in der Welt der Programmierung nicht mehr wegzudenken ist: die objektorientierte Programmierung, kurz OOP. Ich kann diese Art der Programmierung hier nicht in der Ausführlichkeit besprechen, wie sie es verdient hätte, man kann ganze Bücher über OOP schreiben.
Jedoch wäre dies kein Crashkurs über Delphi, wenn die OOP keinen Platz darin hätte. Und so werde ich im Folgenden eine kurze Einfühung zur OOP im Allgemeinen geben und dann aufzeigen, wie sie in Delphi umgesetzt wurde und verwendet werden kann. Der Anfang mag etwas theoretisch sein, jedoch bietet die OOP dem Programmierer enorme Möglichkeiten, sein Programm übersichtlicher und auch zeitsparender zu schreiben.

Ein bisschen Theorie

Nehmen wir als Objekt ein Rechteck her. Dieses Rechteck hat verschiedene Eigenschaften, wie z.B. Höhe und Breite oder auch seine Position im Raum. Auch kann man mit einem Rechteck bestimmt Aktionen verbinden: Man kann es verschieben oder auch seinen Flächeninhalt berrechnen.
Diese Darstellung von Objekten wurde auf die Informatik übertragen. Man ordnet einem Objekt in der Informatik Methoden (das sind an eine Klasse gebundene Prozeduren oder Funktionen) und Eigenschaften (auch „Felder“ genannt) zu (in Delphi gibt es noch „Properties“, das ist aber etwas anderes, daher lasse ich das im Englischen). Dadurch wird ein erster Vorteil der OOP deutlich: sie schafft Ordnung, weil sofort klar wird, welche Methode und welche Information wohin gehören.
Objekte gleicher Art („Rechtecke“) fasst man als „Klasse“ zusammen. Eine Klasse beschreibt also, welche Methoden und Eigenschaften ein solches Objekt habe muss. Ein „wirkliches“, verwendbares Objekt nennt man auch eine „Instanz“ einer Klasse. Man kann beliebig viele Instanzen einer Klasse anlegen. Logisch: Es gibt ja auch beliebig viele Rechtecke. 😉
Elementarer Bestandteil der OOP ist die Vererbung. Um dies deutlich zu machen, ziehen wir ein weiteres Beispiel heran. Man kann sich eine Klasse „geometrische Form“ vorstellen. Diese geometrische Form wird eine Position im Raum besitzen und eine Fläche (welche natürlich auch Null sein kann) und man kann sie auch verschieben. Dabei können wir über die Funktion der Flächenberechnung nur sagen, dass es sie geben wird, aber sie wird für die Klasse der geometrischen Formen noch keine Bedeutung haben. Man bezeichnet sie als „abstrakt„.
Nun kann man von einer Klasse (wie z.B. der geometrischen Formen) weitere Klassen „ableiten„. Das heißt, man bildet eine Klasse als Spezialfall einer anderen Klasse. So ist ein Rechteck ein Spezialfall einer geometrischen Form. Dabei „erbt“ ein Rechteck die Eigenschaft „Position“ und die Methode zur Verschiebung. Es besitzt die neuen Eigenschaften „Höhe“ und „Breite“. Außerdem füllt es die Funktion zur Flächenberechnung mit Leben.
Wir werden später sehen, dass die Vererbung sehr praktisch sein kann. So muss man z.B. die Methode zur Verschiebung einmal schreiben und kann sie dann für alle geometrischen Formen (Rechteck, Kreis, Dreieck, …) verwenden, ohne sie nochmals neu schreiben zu müssen. Auch die Position muss man nicht neu implementieren.
Ein weiterer wichtiger Bestandteil der OOP ist die „Sichtbarkeit„. So werden oft Informationen in Objekten „gekapselt„, das heißt, sie sind nur innerhalb des Objektes sichtbar und können von außen nur über einen festen Satz von Methoden manipuliert werden. Dieser Satz von Methoden stellt dann eine Schnittstelle zwischen der Außenwelt und den Informationen dar.
Dabei gibt es in jeder Programmiersprache verschiedene Stufen der Sichtbarkeit (nicht alle werden in Delphi verwendet). Es gibt Methoden und Eigenschaften, welche nur in der betreffenden Klasse sichtbar sind. Dann gibt es Methoden und Eigenschaften, welche nur in der betreffenden Klasse und in allen abgeleiteten Klassen sichtbar sind. Und es gibt Methoden und Eigenschaften, welche öffentlich sichtbar sind.
Verschiedene Programmiersprachen bieten ja nach ihren Eigenarten noch weitere Arten der Sichtbarkeit und manche Programmiersprachen bieten im Gegenzug auch manche der oben genannten Sichtbarkeiten nicht oder nur in angewandelter Form.
Dies war wirklich nur ein winziges bisschen Theorie, eigentlich nur eine Einleitung, damit die Praxis etwas besser zu verdauen ist. Und mit der möchte ich nun weiter machen, weil dann vieles von dem, was ich oben beschrieben habe, klarer werden wird.

Deklaration und Implementation

Hier gibt es wieder etwas für Sie zu tun, sie können folgende Quelltexte in Ihrem Delphi mitschreiben. Legen Sie dazu erst einmal eine neues Projekt an und speichern Sie es unter einem sinnvollen Namen (also nicht gerade „Project1“ ;-)). Wählen Sie dann im Menü „Datei“->“Neu“->“Unit“. Das Gerüst einer leeren Unit sollte erscheinen, wahrscheinlich mit Namen „Unit2“. Speichern Sie auch diese Datei und zwar unter dem Namen „geomForm.pas“, denn genau das wird diese Datei enthalten: die Klasse für geometrische Objekte. Der neue Name sollte nun auch automatisch im Quelltext verewigt sein.
Zuerst einmal sei hier die Definiton einer Klasse in Delphi gezeigt. Diese Deklaration erfolgt – wie jede andere auch – im interface-Teil des Programmes:

type
  TgeomForm = class
  end;

Dies ist das mindeste, was man für die Definition einer Klasse in Delphi braucht. Damit kann man natürlich noch nicht viel anfangen, denn außer des Namens wurde noch nichts festgelegt. Es sollen nun im folgenden die verschiedenen Eigenschaften und Methoden für eine geometrische Form hinzugefügt werden.
Dabei hat Delphi (bis zu Version 7) die Eigenart, dass die geringste Sichtbarkeit nicht die ist, in der der Elemente nur innerhalb einer Klasse sichtbar sind, sondern die geringste Sichtbarkeit ist die, in der Elemente nur in der Klasse und in anderen Klassen der gleichen Unit sichtbar sind. Diese Sichtbarkeit nennt sich „private„.
Entsprechend gibt es auch die Sichtbarkeit für die Klasse selbst und abgeleitete Klassen, sondern diese Elemente sind auch für alle anderen Klassen in der selben Unit sichtbar! Diese Sichtbarkeit nennt sich „protected„. Dies ist ein deutlicher Unterschied zu anderen Programmiersprachen, wie C++ oder Java, wo die Sichtbarkeiten denselben Namen haben, die Erweiterung der Sichtbarkeit auf die gleiche Unit jedoch nicht vorhanden ist!
Um dieses Problem zu umgehen, kann man einfach für jede Klasse eine Unit reservieren. Dies ist auf Grund der übersichtlichkeit sowieso sehr nützlich und man sollte es sich schon sehr gut überlegt haben, bevor man zwei Klassen in einer Unit anlegt.
Nun zurück zum Beispiel einer geometrischen Form: Die Position soll als private deklariert werden und aus einer x- und einer y-Koordinate bestehen. Diese Koordinaten sollen als Integer deklariert werden und die Pixel auf der Zeichenfläche des Bildschirms darstellen.
Die Definition der Klasse sieht nun so aus:

type
  TgeomForm = class
  private
    Fx : Integer;
    Fy : Integer;
  end;

Man sieht, dass für „Fy“ nicht erneut der Bezeichner „private“ voran gestellt werden musste. Dies liegt daran, dass Delphi immer die Sichtbarkeit des vorangehenden Elementes verwendet, wenn vom Programmierer nichts anderes festgelegt wird. Wird für das erste Element keine Sichtbarkeit angegeben, so wird Standardmäßig „public“ verwendet, das heißt, das Element ist öffentlich sichtbar.
Es ist übrigens kein Schreibfehler, dass den Koordinaten ein „F“ voran gestellt wurde: es ist üblich, als „private“ deklarierte Eigenschaften (nicht die Methoden) mit einem vorangestellten „F“ (für „Feld“ bzw. „field“) zu kennzeichnen. Wozu dies gut ist, wird später noch deutlich werden, wenn die Properties beschrieben werden.
Als nächstes soll die Methode zur Verschiebung einer geometrischen Form eingebaut werden. Dieses Beispiel zeigt auch, wie man die Methoden eines Objektes konkret implementiert. Dazu muss man sie zuerst deklarieren:

type
  TgeomForm = class
  private
    Fx : Integer;
    Fy : Integer;
  public
    procedure verschieben(dx, dy : Integer);
  end;

Achten Sie bitte darauf, dass die Prozedur zum Verschieben öffentlich verwendbar sein soll und daher mit einem vorangestellten „public“ ausgestattet wurde. Ansonsten wird sie wie eine ganz normale Prozedur deklariert.
Nun muss diese Prozedur noch mit Leben gefüllt werden. Dies tut man natürlich im implementation-Teil des Programmes.

procedure TgeomForm.verschieben(dx, dy: Integer);
begin
  self.Fx := self.Fx + dx;
  self.Fy := self.Fy + dy;
end;

Dieser Codeabschnitt wird nun etwas genauer betrachtet. Die erste Zeile gibt an, welche Methode hier implementiert werden soll. Dabei wird die volle Deklaration der Methode (inkl. der Angabe, ob es sich um eine Prozedur oder Funktion handelt) um die Angabe erweitert, zu welcher Klasse die Methode gehört, in diesem Fall „TgeomForm“.
Die Implementation einer Methode folgt ansonsten den gleichen Regeln wie eine normale Prozedur oder Funktion. Jedoch wird oben schon deutlich, dass für die Verwendung innerhalb von Klassen noch ein paar Bezeichner hinzugefügt wurden, so z.B das Wort „self„. Dieses Wort bezeichnet die Instanz, zu der die Methode gehört. Beachten Sie: es bezeichnet wirklich das konkrete Objekt (die Instanz) und nicht nur die Klasse!
Somit wird auch deutlich, wie man auf die Eigenschaften eines Objektes zugreift, sofern sie sichtbar sind: der Name des Objektes und dann – durch einen Punkt getrennt – der Name der Eigenschaft. Entsprechend ruft man übrigens auch die entsprechenden Methoden auf.

Verwendung – Teil 1

Bevor nun die Methode zur Flächenberechnung implementiert will, soll erst einmal die Verwendung einer solchen Klasse demonstriert werden und dann auch noch die Erstellung einer abgeleiteten Klasse vorgestellt werden. Denn das ist die Voraussetzung, um die abstrakte Methode zur Flächenberechnung korrekt einzuführen.
Um eine Klasse zu verwenden, müssen Sie dem Compiler sagen, wo er diese Klasse findet, also in welcher Unit sie deklariert und implementiert wurde. Solche Angaben macht man im „uses„-Abschnitt einer Unit. Wählen Sie die Unit, in welcher sich Ihre Form befindet (wahrscheinlich „Unit1“) und suchen Sie dort den uses-Abschnitt. Erweitern Sie ihn so, dass er wie folgt aussieht:

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, geomForm;

Nachdem der Compiler nun weiß, wo er zu suchen hat, kann man die Klasse verwenden. Es ist egal, wo Sie das tun, man kann es auch direkt bei Erstellung der Form machen. Klicken Sie also doppelt auf „Form1“, um die Methode für das entsprechende „OnCreate“-Ereignis anzulegen. Bauen Sie dann folgenden Quelltext ein, er wird nur ein wenig weiter unten erklärt.
(Folgender Quelltext ist noch nicht ganz korrekt, es fehlt etwas, das sich „Speicherschutzblock“ nennt, darauf werde ich im Kapitel über Exceptions noch eingehen. Es funktioniert auch ohne und für unsere Zwecke reicht das erst einmal.)

procedure TForm1.FormCreate(Sender: TObject);
var geomForm : TgeomForm;
begin
  geomForm := TgeomForm.Create;
  geomForm.verschieben(10,10);
  geomForm.Free;
end;

Was passiert hier? Zuerst einmal wird eine Variable „geomForm“ vom Typ „TgeomForm“ wie jede andere Variable auch deklariert. Doch dies reicht bei Objekten jedoch nicht. Um ein Objekt zu verwenden, muss man es erst noch erstellen. Bei diesem Vorgang erst wird für das Objekt Speicher reserviert. Dies ist übrigens auch der Vorgang, den man als „Instanz erstellen“ bezeichnet.
Das Erstellen eines Objektes funktioniert über einen Konstruktor, was auch nur eine besondere Methode ist und daher genauso verwendet wird. Diese Methode ist in diesem Fall „Create“. Nun stellt sich die Frage, woher diese Methode kommt, denn sie wurde in TgeomForm weder implementiert noch deklariert.
Sie kommt aus der Klasse „TObject“. Denn alle Klassen, bei denen nicht explizit angegeben wird, von welcher sie abgeleitet werden sollen, werden implizit von TObject abgeleitet und erben somit auch alle Methoden und Eigenschaften von TObject. Um hier mal die Delphi-Hilfe zu zitieren: „TObject ist der Ausgangspunkt der Klassenhierarchie, sozusagen der Urahn aller Objekte und Komponenten.“
Nachdem das Objekt erstellt wurde, wird als Beispiel die Methode zum Verschieben aufgerufen. Hier sollten keine weiteren Überraschungen zu sehen sein, der Aufruf ist ansich wie der jeder anderen Methode, nur das man noch das Objekt (nicht die Klasse) angeben muss, zu dem die Methode gehört.
In der nächsten Zeile wird das Objekt „freigegeben„. Das führt dazu, dass das Objekt wieder aus dem Speicher entfernt und und der entsprechende Speicherbereich wieder freigegeben wird. Auch diese Methode stammt von „TObject“ und wird von dort übernommen. Es gibt auch noch die Methode „Destroy“. Diese tut im Prinzip dasselbe, jedoch prüft „Free“ noch, ob das Objekt überhaupt existiert, bevor „Destroy“ aufgerufen wird. Dadurch werden Fehler vermieden. Rufen Sie also immer „Free“ auf.
Nun ist klar, wie man eine Klasse und die daraus instanzierten Objekte verwendet. Nun soll gezeigt werden, wie man von einer Klasse eine andere ableitet. Im folgenden wird dies am Beispiel einer Klasse für Rechtecke demonstriert.

Ableiten von Klassen

Das Ableiten einer Klasse von einer anderen ist nicht sehr schwer. Die Deklaration erfolgt fast genauso wie bei einer „normalen“ Deklaration, mit der Ausnahme, dass man noch angibt, von welcher Klasse die deklarierte Klasse abgeleitet wird. Erstellen Sie eine weitere Unit und speichern Sie als „rechteck.pas“ ab. Erstellen Sie einen uses-Abschnitt, der ausschließlich den Eintrag „geomForm“ enthält. Schreiben Sie außerdem folgendes in den interface-Teil:

type
  TRechteck = class(TgeomForm)
  end;

Damit ist das Ableiten einer Klasse von einer anderen eigentlich schon fertig. „TRechteck“ ist nun ein Nachfahre von „TgeomForm“ und besitzt somit auch die Methode „verschieben“. Nun kann man aber über ein Rechteck ein bisschen mehr aussagen, als man über eine allgemeine, geometrische Form sagen kann. So hat ein Rechteck bespielsweise Höhe und Breite! Und diese beiden Werte sollen in der Klasse „TRechteck“ gespeichert werden.

type
  TRechteck = class(TgeomForm)
  private
    Fhoehe : Integer;
    Fbreite : Integer;
  end;

Diese Eigenschaften (die wiederrum nur in „TRechteck“ und Klassen in der gleichen Unit sichtbar sind) kommen zu den Eigenschaften hinzu, welche schon von „TgeomForm“ geerbt wurden. Nur gewinnen wir damit nichts, weil wir mit „TRecheck“ noch nicht mehr anfangen können als mit „TgeomForm“.

Abstrakte Methoden

Um einen Gewinn zu erreichen, erweitern wir nun „TgeomForm“ erst einmal um eine abstrakte Methode, also eine Methode, welche in „TgeomForm“ zwar deklariert aber noch nicht implementiert wird. Sie ahnen es sicherlich schon, es geht um die Flächenberechnung für ein geometrisches Objekt. Erweitern Sie die Klasse „TgeomForm“ so, dass sie wie folgt aussieht:

type
  TgeomForm = class
  private
    Fx : Integer;
    Fy : Integer;
  public
    procedure verschieben(dx, dy : Integer);
    function flaeche : Integer; virtual; abstract;
  end;

Das Bedarf nun doch einiger Erklärung. Der erste Teil sollte klar sein: Die Klasse wird um eine Funktion mit Namen „flaeche“ erweitert, welche einen Integer zurückliefert – eben die Fläche. Aber was bedeuten die beiden Schlüsselworte dahinter?
Zuerst einmal zum Wort „virtual„. Dies bedeutet, dass diese Methode in von „TgeomForm“ abgeleiteten Klassen überschrieben (also mit einer neuen Bedeutung versehen) werden darf. Die Methode aus der Mutterklasse ist in der abgeleiteten Klasse dann nicht mehr zu sehen, nur die „neue“ Methode gleichen Namens. Mehr dazu gibt es noch im nächsten Abschnitt.
Das Wort „abstract“ sollte nach dem Theorie-Teil schon fast selbst erklärend sein. Es signalisiert dem Compiler, dass es sich bei der Methode „flaeche“ um eine abstrakte Methode handelt, sie also in „TgeomForm“ nur deklariert, aber nicht implementiert (mit Leben gefüllt) wird. Das wird dann in einer der abgeleiteten Klassen gemacht, was ja auch der Plan war, als wir anfingen, die Klasse „TgeomForm“ zu erweitern. 😉
Und diesen Plan werden wir nun vervollständigen, indem wir schlussendlich die Methode zur Flächenberechnung in der Klasse „TRechteck“ deklarieren und implementieren. Die Deklaration sieht so aus:

type
  TRechteck = class(TgeomForm)
  private
    Fhoehe : Integer;
    Fbreite : Integer;
  public
    function flaeche : Integer; override;
  end;

Eine Mehtode, welche in der Mutterklasse als „abstract“ deklariert wurde, muss also in der abgeleiteten Klasse, sofern sie dort implementiert werden soll, nochmals deklariert werden. Jedoch reicht eine einfache Deklaration nicht aus, sondern es muss auch noch das Schlüsselwort „override“ hinzugefügt werden. Das signalisiert dem Compiler, dass die Methode der Mutterklasse überschrieben werden soll.
Nun muss diese Methode noch implementiert werden, natürlich im implemenation-Teil der Unit:

function TRechteck.flaeche: Integer;
begin
  result := Fhoehe*Fbreite;
end;

Diese Funktion würde nun also die Fläche des Rechtecks zurück geben. Wenn nur irgendwo jemals definiert worden wäre, welche Höhe und Breite das Rechteck hat. Denn so, wie die Klasse momentan aussieht, kann man das gar nicht bestimmen, Höhe und Breite sind immer Null! Darum müssen Sie sich also noch kümmern. Fürs Erste wird es ausreichen, wenn man beim Erstellen (Sie erinnern sich: „Create“) angeben kann, wie groß das Rechteck sein muss. Doch bevor wir uns darum kümmern, noch ein kleiner Exkurs zum Überschreiben von Methoden.

Methoden überschreiben – Ein bisschen ausführlicher

Im vorigen Abschnitt wurde gezeigt, wie man eine abstrakte Methode überschreiben kann. Dies geht jedoch auch mit nicht-abstrakten Methoden, also mit Methoden, die in der Mutterklasse implementiert wurden. Das macht die ganze Sache etwas kompliziert.
Sehen wir uns die folgende Verwendung an, dieses Mal mit einem Beispiel, welches nicht in den Rest passt:

type
TAuto = class(TObject)
private
public
function drive : String;
end;

{…}

function TAuto.drive : String;
begin
result := ‚TAuto.drive‘;
end;
type
TOpel = class(TAuto)
private
public
function drive : String;
end;

{…}

function TOpel.drive: String;
begin
result := ‚TOpel.drive‘;
end;
Und dazu folgende Verwendung der beiden Klassen:

var auto : TAuto;
begin
  auto := TOpel.Create;
  ShowMessage(auto.drive());
end;

Beachten Sie dabei, dass „auto“ zwar als „TAuto“ deklariert wurde, aber der Konstruktor von „TOpel“ aufgerufen wird, außerdem sollten Sie beachten, dass die Methode in „drive“ in der Klasse „TAuto“ nicht als „virtual“ deklariert wurde.
Führt man diesen Code nun aus, sieht man, dass die Methode „drive“ aus der Klasse „TAuto“ aufgerufen wird, obwohl wir eine Instanz der Klasse „TOpel“ erzeugt haben. Methoden mit diesem Verhalten nennt man „statisch„, es ist die Standardeinstellung für Methoden. Bei statischen Methoden wird also immer die Methode der Klasse aufgerufen, die deklariert („auto : TAuto“) wurde und nicht die Methode der Klasse, die instanziert wurde.
Deklariert man die Methode „drive“ in der Klasse „TAuto“ nun als „virtual“ und ergänzt man die Methode „drive“ in der Klasse „TOpel“ um ein „override“, so ergibt obiger Quelltext ein anderes Ergebnis: Dann wird nämlich die Methode aus der Klasse „TOpel“ ausgeführt, also der Klasse, die wir instanziert haben! Die „virtuelle“ Methode aus der Klasse „TAuto“ wurde also „überschrieben“ (override).
Es gibt übrigens außer virtuellen Methoden auch noch „dynamische“ Methoden, welche mittels des Schlüsselwortes „dynamic“ an der Stelle von „virtual“ deklariert werden. Dynamische Methoden unterscheiden sich in der Verwendung nicht von virtuellen, der Unterschied liegt lediglich in der internen Umsetzung: Virtuelle Methoden sind auf eine hohe Geschwindigkeit optimiert, dynamsiche Methoden auf einen geringen Speicherverbrauch.
Nun sollte der Quelltext aus dem vorigen Abschnitt klarer werden: Die Methode „flaeche“ in TgeomForm ist virtuell, weil sie von den abgeleiteten Klassen überschrieben werden muss. Dies macht Sinn, denn sie ist auch abstrakt (also nicht implementiert) ein Aufruf dieser Methode würde daher nicht nur keinen Sinn machen, sondern einen Fehler produzieren. Was nicht da ist, kann man nicht aufrufen. Daher müssen abstrakte Methoden auch immer als virtuell oder dynamisch deklariert werden.
Und nun weiter mit unserem bisherigen Beispiel!

Der Konstruktor

Wie bereits gesagt, ist der Konstruktor der Teil einer Klasse, welcher neue Instanzen eben dieser Klasse erzeugt. Er ist also nicht an ein Objekt, sondern an die entsprechende Klasse gebunden. Der „Ur“-Konstruktor ist in der Urklasse „TObject“ deklariert, aber er kann in jeder Klasse neu deklariert und implementiert werden, solange man daran denkt, jeweils den Konstruktor der Vorfahrklasse auch noch auszurufen. Dies muss man tun, damitr auch das, was in den Muttterklassen im Konstruktor gemacht wird, erledigt wird.
In unserem Fall möchten wir, dass der Konstruktor auch noch das Setzen von Höhe und Breite mit erledigen soll. Dazu soll der Konstruktor nicht „normal“ aufgerufen werden, sondern direkt noch die beiden Maße als Parameter mitgegeben bekommen. Dazu muss man den Konstruktor erst einmal wieder deklarieren. Dabei wird ein Konstruktor weder als Funktion noch als Prozedur deklariert, sondern als … Konstruktor. 😉

type
  TRechteck = class(TgeomForm)
  private
    Fhoehe : Integer;
    Fbreite : Integer;
  public
    constructor create(hoehe, breite : Integer);
    function flaeche : Integer; override;
  end;

Bis auf das Schlüsselwort „constructor“ sieht der Konstruktor in der der Deklaration also aus, als wäre er eine ganz normale Methode. Was er nicht ist, denn, wie bereits erwähnt, er ist nicht an eine Instanz, sondern an die Klasse gebunden. Nun zur Implementation, die eigentlich auch keine Überraschungen bereit hält:

constructor TRechteck.create(hoehe, breite: Integer);
begin
  inherited create;

  FHoehe := hoehe;
  FBreite := breite;
end;

Das einzig wirklich neue an diesem Quelltext ist die Zeile, welche mit den Schlüsselwort „inherited“ beginnt. Leitet man den Aufruf einer Methode (in diesem Fall „create“) mit dem Schlüsselwort „inherited“ ein, so signalisiert man dem Compiler damit, dass in diese Fall die entsprechende Methode der Mutterklasse aufgerufen werden soll und nicht die Methode der aktuellen Klasse. Hier heißt das also: es wird der Konstruktor von „TgeomForm“ aufgerufen.
Der Rest des Konstruktors ist einfach, es werden die per Parameter übergebenen Maße in den entsprechenden Eigenschaften gespeichert, damit sie später zur Verfügung stehen. Ich habe an dieser Stelle aus Absicht darauf verzichtet, über „self.FHoehe“ bzw. „self.Fbreite“ auf die Eigenschaften zuzugreifen, um Ihnen zu zeigen, dass es bei Eindeutigkeit der Bezeichner auch anders geht.

Objekte sind Referenzdatentypen

An dieser Stelle möchte ich noch auf einen Fehler hinweisen, der sehr gerne begangen wird, nämlich dann, wenn es um die Zuweisung von Objekten geht. Nehmen wir nur für das nachfolgende Beispiel an, die Höhe und Breite eines Rechtecks könnten von außen (über „hoehe“ und „breite“) manipuliert werden. Nehmen wir weiterhin an, es wurden zwei Rechtecke deklariert, eines wurde erzeugt:

var r1, r2 : TRechteck;
begin
  r1 := TRechteck.Create(50, 10);
  r2 := r1;
  r2.hoehe := 10;
  r2.breite := 10;
  ShowMessage(IntToStr(r1.flaeche));
end;

Die Meldung zeigt in diesem Fall nicht „500“, wie man zuerst denken würde, da „r1“ ja mit den Maßen 50 und 10 erzeugt wurde, sondern die Meldung zeigt „100“, was der Fläche von „r2“ entspricht. Der Grund dafür ist, dass Objekte „Referenzdatentypen“ sind und sich bei Zuweisungen so verhalten, wie ich es auch schon bei Arrays beschrieben habe. Die Zuweisung „r2 := r1“ erzeugt kein neues Objekt, sondern „r2“ verweist auf dasselbe Objekt wie „r1“, ist sozusagen nur ein anderer Name für dasselbe Objekt, weshalb die Fläche hinteher auch 100 und nicht 500 ist.
Der Konstuktor ist nun beschrieben, jetzt werfen wir nochmals einen Blick auf die Verwendung der neu erstellten Objekte. Allerdings nicht bevor wir uns nicht das Gegenteil des Konstruktors angesehen haben. 😉

Der Destruktor

Obwohl in diesem Beispiel nicht benötigt, sei hier noch kurz auf das Gegenstück zum Konstruktor, den „Destruktor“ eingegangen. Er ist dafür zuständig, dass das Objekt, welches zuvor durch den Konstruktor erzeugt wurde, nach Verwendung auch wieder aus dem Speicher entfernt wird. Dabei wird der Destruktor als Methode der Instanz aufgerufen, welche „vernichtet“ werden soll.
Der Destruktor wird fast wie der Konstruktor deklariert, mit den Ausnahmen, dass anstatt des Schlüsselwortes „constructor“ das Schlüsselwort „destructor“ verwendet wird und er den Destruktor der Mutterklasse überschreibt, also mit einem „override“ deklariert wird. Dies ist nur beim Destruktor nötig, da der Konstruktor ja über die Klasse und nicht über eine Instanz aufgerufen wird und somit eindeutig ist, welcher Konstruktor aufgerufen wird. Der Name des Destruktors ist immer „destroy“.

destructor destroy; override;

Wichtig ist, dass im Destruktor ebenfalls der Destruktor der Vorwahrklasse aufgerufen wird, jedoch am Ende des eigenen Destruktors:

destructor TmyClass.destroy;
begin
  {...}
  inherited destroy;
end;

In „Ihrem“ Destruktor sollten Sie vorm Aufruf des Destruktors der Mutterklasse allen Speicher freigeben, den Sie innerhalb der Instanz, welche freigegeben werden soll, belegt haben. Meist wird es sich dabei um weitere Objekte handeln, welche Sie in Ihrer Klasse instanzieren.
Überschreiben und implementieren Sie immer den Destruktor „destroy“, aber niemals die Methode „free „. Diese wird zwar immer aufgerufen, wenn man ein Objekt freigeben möchte, diese ruft aber wiederrum „destroy“ auf. Die Methode „free“ enthält lediglich noch eine Überprüfung, ob das Objekt, welches freigegeben werden soll, überhaupt noch existiert und vermeidet somit Fehler. Also: Finger davon, die eigentliche Arbeit wird in „destroy“ erledigt.
Da wir in diesem Beispiel keinerlei Objekte innerhalb unserer Klassen instanzieren, sondern lediglich primitive Datentypen verwenden, brauchen wir in diesen Klassen auch keine Destruktoren.

Bitte beachten Sie zum Freigeben von Objekten auch den Teil dieses Crashkurses über Exceptions!

Verwendung – Teil 2

Die Verwendung soll noch einmal die „Verwandschaft“ von Klassen verdeutlichen. Eine kleine Vorbereitung müssen Sie jedoch noch vornehmen, platzieren Sie bitte eine Label, zwei Editfelder und einen Button auf der Form. Ändern Sie den Namen der Editfelder über den Objektinspektor auf „ed_hoehe“ und „ed_breite“ (die entsprechende Eigenschaft heißt „name“) und sorgen Sie dafür, dass beide Editfelder beim Programmstart leer sind, indem Sie die Eigenschaft „text“ entsprechend ändern. Geben Sie dem Label den Namen „la_flaeche“, es soll anfangs keinen Text anzeigen (Eigenschaft „Caption“ ändern). Den Button nennen Sie „bt_flaeche“, seinen Titel ändern Sie auf „Fläche berechnen“.
Klicken Sie nun doppelt auf den Button, um die OnClick-Methode aufzurufen. Deklarieren Sie dort die Variablen „hoehe“, „breite“ und „fleche“ als Integer. Lesen Sie mittels „StrToInt“ die Höhe und die Breite aus den entsprechenden Editfeldern ein! Deklarieren Sie außerdem noch eine Variable „rechteck“ vom Typ „TRechteck“. Bei einem Objekt wie „rechteck“ reicht es, wie inzwischen bekannt sein sollte, jedoch nicht aus, dieses nur deklariere, Sie müssen es auch noch erzeugen. Somit sieht die OnClick-Methode bisher so aus:

procedure TForm1.bt_flaecheClick(Sender: TObject);
var hoehe, breite, flaeche : Integer;
    rechteck : TRechteck;
begin
  hoehe := StrToInt(ed_hoehe.Text);
  breite := StrToInt(ed_breite.text);

  rechteck := TRechteck.create(hoehe, breite);

Wie man an die Fläche herankommt, sollte klar sein:

flaeche := rechteck.flaeche;

Schließlich noch das Objekt freigeben und das Ergebnis ausgeben:

 rechteck.free;
  la_flaeche.Caption := IntToStr(flaeche);
end;

Ein Rechteck ist auch eine geometrische Form

Hier wird jedoch noch nicht so ganz deutlich, welchen Vorteil der Vererbung liefert. Das wird erst deutlich, wenn wir noch eine weitere Klasse einführen, nämlich die Klasse „TKreis“. Wie das geht, sollte klar sein. Ein Kreis hat einen Radius anstatt Höhe und Breite, entsprechend müssen Eigenschaften, Konstruktor und die Methode zur Berechnung der Fläche angepasst werden.
Ist das geschehen, kann man sich folgende Situation vorstellen: Man schreibt ein Grafikprogramm und möchte geometrische Formen verwalten, wobei es egal sein soll, ob es sich dabei um Rechtecke, Kreise oder noch andere Formen handelt. Eine Anwendung, welche das Grafikprogramm bieten soll, ist, die Gesamtfläche aller Formen zu errechnen. Mit OOP und Vererbung kein Problem!

interface

type
  TgeomFormArray = Array of TgeomForm;

implementation

function gesamtflaeche (formen : TgeomFormArray) : Integer;
var i : Integer;
begin
  result := 0;

  for i:=0 to High(formen) do
    result := result + formen[i].flaeche;
end;

Ich habe hier nur einen Codeschnipsel aufgeschrieben, dessen Hauptaussage aber klar sein sollte. Zuerst wird ein neuer Typ definiert, ein Array aus geometrischen Formen. Dies ist nötig, damit man hinterher eine Variable diesen Typs an eine Funktion übergeben kann, definiert man dafür keinen neuen Typ, macht Delphi Probleme. Eine Variable diesen Typs wird dann der Funktion „gesamtflaeche“ übergeben, diese Variable soll alle Formen enthalten, die im Programm verwendet werden (also z.B. fünf Rechtecke und drei Kreise).
Sie bemerken: das Array ist ein Array von „TgeomForm“, enthält aber Daten der Typen „TRechteck“ und „TKreis“! Dies wird nochmals in der Schleife deutlich, welche alle Elemente des Arrays durchläuft: Durchlaufen wird ein Array von geometrischen Formen, für jede dieser Formen wird die Funktion „flaeche“ aufgerufen. Dies geht, weil die Funktion „flaeche“ abstrakt in „TgeomForm“ deklariert wurde und somit bekannt ist, dass jedes dieser Elemente sie besitzt. Der Code, welcher beim Aufruf ausgeführt wird, ist jedoch jener der Datentypen „TRechteck“ bzw. „TKreis“, je nachdem, das die aktuelle geometrische Form ist!
Nun sollte klar sein, welcher Vorteil die Vererbung bietet: Eine abgeleitete Klasse kann die Methoden der Mutterklasse überschreiben und ihnen somit eine völlig neue Bedeutung geben. Dies nennt man übrigens auch „Polymorphie„. Bei einer abstrakten Methode, wie in diesem Fall, wird durch der Methode das Überschreiben überhaupt erst eine Implementation gegeben, man könnte aber auch eine bereits implementierte Methode überschreiben, sofern auch sie mit dem Schlüsselwort „virtual“ deklariert wurde.

Properties

Bisher wurde nur gezeigt, wie man von außen Methoden verwendet, jedoch nicht, wie man von außen die Eigenschaften von Objekten manipuliert. Es hat sich durchgesetzt, dass man auch die Eigenschaften, welche eigentlich von außen manipuliert werden dürfen (also nicht „private“ oder „protected“ wären), nicht einfach in den public-Teil zu schreiben, sondern sie der Außenwelt durch so genannte „properties“ zur Verfügung zu stellen. Damit behält der Programmierer der entsprechenden Klasse die Kontrolle darüber, was mit den Eigenschaften geschieht.
Im Folgenden soll erst einmal die Deklaration einer property gezeigt werden, wieder anhand der Klasse „TgeomForm“.

type
  TgeomForm = class
  private
    Fx : Integer;
    Fy : Integer;
  public
    procedure verschieben(dx, dy : Integer);
    function flaeche : Integer; virtual; abstract;

    property x : Integer read Fx write Fx;
    property y : Integer read Fy write Fy;
  end;

Die Deklaration ist recht einfach zu verstehen: Das Schlüsselwort „property“ signalisiert dem Compiler, was nun auf ihn zukommt, dann folgen Name und Typ der property. Anstatt hier jedoch Schluss zu machen, benötigt der Compiler nun die Information, wohin er die Aufrufe der property „umleiten“ soll, denn eine property ansich enthält keinen Wert, sie holt ihn nur woanders her bzw. setzt ihn woanders.
Woher eine property ihren Wert holen oder wo sie einen neuen Wert setzen soll, das wird über die Schlüsselwörter „read“ und „write“ festgelegt. In diesem Fall wird der Wert für die property „x“ aus der privaten Eigenschaft „Fx“ geholt und wenn „x“ ein Wert zugewiesen wird, wird diese Zuweisung auch dorthin „weitergeleitet„. Hier wird übrigens auch klar, weshalb man bei privaten Eigenschaften noch ein „F“ vor den Namen schreibt: damit man hinterher eine property mit dem richtigen Namen einführen kann. Verwendet wird eine solche property übrigens wie folgt:

var myGeomForm : TgeomForm;
{...}
myGeomForm.x := 5;

Also nichts anderes als bei den privaten Eigenschaften auch. Jetzt stellen Sie sich wahrscheinlich die Frage, wo denn nun der Vorteil von properties liegt. Das wird deutlich, wenn man die Deklaration mal ändert:

type
  TgeomForm = class
  private
    Fx : Integer;
    Fy : Integer;
    procedure setX(const value : Integer);
    function getX : Integer;
  public
    procedure verschieben(dx, dy : Integer);
    function flaeche : Integer; virtual; abstract;

    property x : Integer read getX write setX;
    property y : Integer read Fy write Fy;
  end;

Hier wurde nur die Deklaration für die property „x“ verändert: Es wird von „x“ aus nun nicht mehr auf die private Eigenschaften „Fx“ zugegriffen, sondern die Aufrufe werden an die Funktion „getX“ und die Prozedur „setX“ weitergeleitet. Wird also „x“ wieder (wie oben bereits gezeigt) der Wert 5 zugewiesen, wird nun die Prozedur „setX“ aufgerufen und als Parameter „value“ die 5 übergeben. Wird „x“ abgefragt, so wird die Funktion „getX“ aufgerufen und das Ergebnis als „x“ zurückgegeben.
Der Vorteil dieser Technik zeigt sich in der Implementation der Methode „setX“:

procedure TgeomForm.setX(const value: Integer);
begin
  if value >= 0 then
    Fx := value;
end;

In dieser Prozedur wird also zuerst geprüft, ob „x“ auf einen plausiblen Wert gesetzt werden soll (in diesem Fall sollen alle Werte größer oder gleich Null sein), und nur wenn dies der Fall ist, wird der Wert im privaten „Fx“ auch wirklich gesetzt, der Wert also letzten Endes geändert.
Das Verwenden von get- und set-Methoden gibt dem Programmierer einer Klasse also Möglichkeiten der Kontrolle, zusätzlich zu der Möglichkeit, den Hinterbau einer Eigenschaft (z.B. „x“) komplett zu verändern (in diesem Fall wurde der Hinterbau auf die Methoden „getX“ und „setX“ umgestellt), ohne dass derjenige, der die Klasse nur verwendet, etwas bemerkt.

Array-Properties

Eine Erweiterung der normalen Properties stellen die Array-Properties dar. Mit ihnen hat man die Möglichkeit, beim Zugriff auch noch einen oder mehrere Indizies anzugeben, also auf die Property zuzugreifen wie auf ein Array. Folgendes Beispiel soll den Zugriff auf x- und y-Position einer geometrischen Form mittels Indizes aufzeigen, dabei soll man die x-Position mit dem Index „0“ erreichen und die y-Koordinate mittels „1“:

type
  TgeomForm = class
  private
    Fx : Integer;
    Fy : Integer;
    procedure setPos(index : Integer; const value : Integer);
    function getPos(index : Integer) : Integer;
  public
    {...}
    property position[index : Integer] : Integer read getPos write setPos;
  end;

Die get- und set-Methoden erhalten also einen zusätzlichen Parameter, der den übergebenen Index angibt. Eine Implementation sähe dann (am Beispiel der set-Methode) so aus:

procedure TgeomForm.setPos(index : Integer; const value : Integer);
begin
  case index of
    0: Fx := value;
    1: Fy := value;
  end;
end;

Ein Zugriff auf diese Property könnte so aussehen:

myGeomForm.position[0] := 3;

Es kann sinnvoll sein, mittels der „default„-Direktive eine Kurzform einzuführen und direkt über den Namen der Klasseninstanz auf eine Property zuzugreifen:

property position[index : Integer] : Integer read getPos write setPos; default;
myGeomForm[0] := 3;

Obiger Zugriff ist identisch mit dem Zugriff auf „position“, da die Property „position“ als Default-Property festgelegt wurde. Selbstverständlich kann es nur eine Default-Property pro Klasse geben!

Ereignisse

Ich möchte an dieser Stelle eine besondere Art von Properties aufzeigen, nämlich die Ereignisse. Dabei möchte ich nur recht oberflächlich vorgehen, eine tiefergehende Beschreibung ist im Rahmen dieses Crashkurses nicht sinnvoll.
Ereignisse sind in der Programmierung unter Windows sehr wichtig, weil sie die beste Möglichkeit sind, auf die Eingaben des Nutzers zu reagieren. Ein Ereignis wird bespielsweise ausgelöst, wenn der Nutzer auf einen Button klickt: Das Ereignis „OnClick“ des entsprechenden Buttons wird ausgelöst.
Nun könnte man in einer Klasse einfach eine Methode einbauen, welche ausgeführt wird, wenn ein Ereignis eintritt. Jedoch ist dies nicht wirklich praktikabel, weil dann bei jedem Button, auf den geklickt wird, immer dasselbe passieren würde, was natürlich Unsinn wäre. Man braucht also eine Methode, welche für jede Instanz unterschiedlich ist. Und genau das ist ein Ereignis: eine Property, welche eine Methode enthält!
Dabei ist genau festgelegt, wie eine Methode, die einem Ereignis zugewiesen wird, auszusehen hat. So muss die Methode, welche einem OnClick-Ereignis zugewiesen wird, eine Prozedur mit einem Parameter vom Typ TObject sein. Wird nun das Ereignis ausgelöst, wird die Methode ausgeführt, welche dem Ereignis zugewiesen wurde.
Das ist sich erst einmal merkwürdig anhören und soll daher am nachfolgenden Beispiel genauer erläutert werden. Erstellen Sie dazu eine neue Delphi-Anwendung und platzieren Sie einen Button auf der Form. Klicken Sie nun doppelt auf eine freie Stelle der Form. Sie sehen nun den Codeeditor vor sich, der Cursor blinkt in der Methode „FormCreate“ der Form1: diese Methode wurde intern dem OnCreate-Ereignis von Form1 zugewiesen, wird also aufgerufen, wenn die Form erzeugt wird.
Fügen Sie unter der „FormCreate“-Methode eine neue Methode ein:

procedure TForm1.doSomething(Sender: TObject);
begin
  ShowMessage('Foo');
end;

Die Methode sollte über dem „end.“ am Ende der Unit stehen, und muss natürlich noch im interface-Abschnitt deklariert werden, im public-Bereich von Form1.
Fügen Sie nun noch in die Methode „FormCreate“ den folgenden Code ein:

Button1.OnClick := doSomething;

Starten Sie nun das Programm und klicken Sie den Button an. Wenn alles korrekt ist, wird Ihnen nun eine Nachrichtenbox mit dem Text „foo“ entgegen springen. Das, was sonst Delphi intern für Sie erledigt, wenn Sie doppelt auf einen Button klicken (nämlich eine Methode für das OnClick-Ereignis anlegen und zuweisen), haben Sie nun manuell gemacht: Sie haben die Methode „doSomething“ angelegt und bei Erstellen der Form dem OnClick-Ereignis des Buttons zugewiesen. Sie wird ausgeführt, wenn der Button geklickt wird.
Sie werden bemerken, dass die Methode „doSomething“ einen Parameter „Sender : TObject“ hat, der überhaupt nicht genutzt wird. Der Grund dafür ist die oben bereits erwähnte Vorschrift, wie ein Ereignis auszusehen hat. Ein Ereignis hat – wie jede andere Property – einen Typ, so ist z.B. ein OnClick-Ereignis vom Typ TNotifyEvent, welches wie folgt deklariert ist:

TNotifyEvent = procedure(Sender: TObject) of object;

Und wie Sie einer Property vom Typ „Integer“ auch nur einen Integer zuweisen können, so können Sie einem TNotifyEvent auch nur ein TNotifyEvent zuweisen: Eine Prozedur mit einem Parameter vom Typ „TObject“, welche an eine Klasse gebunden ist („of object“), also eine Methode ist.
Im normalen Delphi-Betrieb ist es das einfachste, die IDE die entsprechenden Methoden anlegen zu lassen und den Ereignissen zuweisen zu lassen. Der Objektinspektor zeigt Ihnen zu jedem Objekt auf der Form auch die verfügbaren Ereignisse an: Objekt markieren, im Objektinspektor die Karteikarte „Ereignisse“ wählen. Möchten Sie einem Ereignis eine Methode zuweisen, klicken Sie doppelt auf das leere Feld neben dem Namen des Ereignisses. Eine Methode wird angelegt und dem Ereignis zugewiesen. Bestehende Methoden können Sie einem Ereignis zuweisen, indem Sie nicht doppelt klicken, sondern mittels der DropDown-Liste eine bestehende Methode auswählen.
Mehr soll an dieser Stelle nicht zu Ereignissen gesagt werden, obiges sollte Sie in die Lage versetzen, einen Großteil der Aufgaben zu erledigen.

Klassenmethoden

Es kann manchmal sinnvoll sein, dass man eine Methode nicht über die Instanz einer Klasse aufrufen möchte, sondern nur über die Klasse. So könnte man sich vorstellen, dass die Klasse „TgeomForm“ eine Methode „dimension“ besitzt, welche zurückgibt, welche Dimension die geometrischen Objekte besitzen, die durch diese Klasse dargestellt werden. Das Ergebnis dieser Methode wäre nicht abhängig von einer Instanz, sondern für die gesamte Klasse identisch.
Möchte man eine solche Methode haben, so leitet man sie bei der Deklaration mit dem Schlüsselwort „class“ ein.

type
  TgeomForm = class
  {...}
  public
    class function dimension : Integer;
  end;

{...}

class function TgeomForm.dimension : Integer;
begin
  result := 2;
end;

Selbstverständlich kann man in Klassenmehtoden nicht auf die Eigenschaften einer Klasse zuzugreifen, da diese nur existieren, wenn man mit Instanzen arbeitet!

Vorteile der OOP

Hier möchte ich noch einmal zusammentragen, was die Vorteile der OOP sind. Ich hoffe, sie haben das bereits selbst erkannt, aber es kann nichts schaden, Ihnen das alles noch einmal in Erinnerung zu rufen! 😉

  1. Zuerst einmal wäre die Kapselung. Mit ihr hat man die Möglichkeiten, Informationen innerhalb einer Klasse zu verbergen und nur ausgewählte Methoden und Eigenschaften nach außen hin sichtbar und somit nutzbar zu machen.
  2. Die Vererbung ist der nächste Schritt. Mit ihr ist es möglich, Klassen von anderen Klassen abzuleiten und alle Methoden und Eigenschaften der Mutterklasse zu übernehmen. Sie müssen also nur in der Mutterklasse deklariert und implementiert werden und sind in allen abgeleiteten Klassen vewendbar. Änderungen an den Methoden und Eigenschaften in der Mutterklasse wirken sich auf alle abgeleiteten Klassen aus.
  3. Abstrakte Methoden bieten die Möglichkeit, einen Prototyp zu schaffen und ihn erst in abgeleiteten Klassen zu implementieren. Zusammen mit der Polymorphie (also das Überschreiben von Methoden der Mutterklasse in abgeleiteten Klassen) erhält man ein mächtiges Werkzeug zur effizienten Verwendung von Klassen
  4. Mit properties gewährt man dem Nutzer einer Klasse kontrollierten Zugriff auf Eigenschaften (die noch nicht einmal existieren müssen, siehe get- und set-Methoden). Properties zeigen außerdem sehr gut, dass man die Implementation einer Klasse vollständig ändern kann, ohne dass es der Nutzer eine Klasse bemerkt. Einzige Bedingung: Das für den Nutzer sichtbare Interface muss unverändert bleiben. Wie bei den Properties.

Dies sind insgesamt vier Punkte, die hier aufgezählt wurden. Die OOP bietet noch sehr viel mehr Vorteile, aber da die OOP hier nur sehr knapp beschrieben wurde, können auch nicht alle Vorteile hier aufgezählt werden. Ich möchte Sie bitten, sich mittels weiterer, auf OOP spezialisierter Tutorials und evtl. Bücher tiefer in das Thema einzuarbeiten, falls Sie Interesse daran haben. Zu empfehlen ist es allemal, OOP erleichert die Arbeit enorm!

2 Gedanken zu „Delphi-Crashkurs“

  1. Guten Morgen,

    erst einmal vielen Dank für die ausführlichen Erklärungen!

    Im ersten Beispiel zur Darstellung von Zahlen im Rechner müsste es heißen

    4 mal 1 = 4*10^0

    statt

    4 mal 1    = 4*10

     

    1. Vielen Dank für den Hinweis.
      Ich habe es entsprechend korrigiert.
      Auch Deinen weiteren Hinweis habe ich eingearbeiotet.

Kommentare sind geschlossen.