Logo

Marco Cantù's
Essential Pascal

Kapitel 6
Prozeduren und Funktionen

Eine weitere wichtige Idee, die Pascal kennzeichnet, ist das Konzept der Routine, grundsätzlich eine Folge von Anweisungen mit einem eindeutigen Namen, welche beliebig oft unter Verwendung dieses Namens aktiviert werden kann. Auf diese Weise vermeiden Sie die ständige Wiederholung gleicher Anweisungen, Sie haben nur eine einzige Version des Codes für das gesamte Programm, den Sie leicht verändern können. Von diesem Blickpunkt aus können Sie sich eine Routine als den grundlegenden Mechanismus zur Kapselung betrachten. Ich werde auf dieses Thema mit einem Beispiel zurückkommen, nachdem ich die Syntax der Pascal-Routinen vorgestellt habe.

Pascal Prozeduren und Funktionen

In Pascal kann eine Routine zwei Formen annehmen: eine Prozedur oder eine Funktion. Der Theorie nach ist die Prozedur eine Operation, die der Computer für Sie ausführen soll, die Funktion ist eine Berechnung, die einen Wert zurückliefert. Dieser Unterschied wird dadurch gekennzeichnet, dass eine Funktion ein Ergebnis, einen Rückgabewert besitzt, eine Prozedur hingegen nicht. Beide Arten von Routinen können mehrere Parameter von verschiedenen Datentypen besitzen.

In der Praxis hingegen ist der Unterschied zwischen Funktionen und Prozeduren sehr gering: Sie können eine Funktion aufrufen, um einige Arbeit ausführen zu lassen und danach das Ergebnis (welches ein optionaler Fehlercode oder etwas ähnliches sein kann) übergehen oder Sie können eine Prozedur aufrufen, die ein Ergebnis innerhalb ihrer Parameter zurückgibt (mehr über Referenzparameter später in diesem Kapitel).

Hier ist die Definition einer Prozedur und zwei Versionenen der gleichen Funktion, jeweils unter Verwendung einer leicht abweichenden Syntax:

procedure Hello;
begin
  ShowMessage ('Hallo Freunde!');
end;

function Double (Value: Integer) : Integer;
begin
  Double := Value * 2;
end;

// oder als Alternative
function Double2 (Value: Integer) : Integer;
begin
  Result := Value * 2;
end;

Die Verwendung von Result anstatt der Zuweisung des Rückgabewertes an den Funktionsnamen ist sehr beliebt geworden und führt meiner Meinung nach dazu, dass der Code leichter lesbar ist.

Nachdem diese Routinen definiert wurden können Sie sie ein- oder mehrmals aufrufen. Sie rufen die Prozedur auf, um ihre Aufgabe auszuführen und benutzen die Funktion, um einen Wert zu berechnen:

procedure TForm1.Button1Click (Sender: TObject);
begin
  Hello;
end;
 
procedure TForm1.Button2Click (Sender: TObject);
var
  X, Y: Integer;
begin
  X := Double (StrToInt (Edit1.Text));
  Y := Double (X);
  ShowMessage (IntToStr (Y));
end;

Hinweis: Kümmern Sie sich im Moment nicht um die Syntax der beiden oberen Prozeduren, die eigentlich Methoden sind. Setzen Sie einfach zwei Schalter auf ein Delphi-Formular und klicken Sie auf diese zur Entwurfszeit. Die Delphi IDE wird dadurch den korrekten Code-Rahmen erzeugen: Nun müssen Sie nur einfach die Zeilen zwischen begin und end füllen. Um den oberen Code übersetzen zu können, müssen Sie zusätzlich noch ein Eingabefeld in das Formular einfügen.

Jetzt können wir auf das Konzept der Code-Kapselung zurückkommen, welches ich bereits zuvor angeschnitten hatte. Wenn Sie die Funktion Double aufrufen, so müssen Sie nichts über den Algorithmus wissen, der zu ihrer Implementation verwendet wurde. Wenn Sie später einen besseren Weg zur Verdopplung von Zahlen finden, so können Sie einfach den Code der Funktion verändern, der aufrufende Code wird davon unberührt sein (obwohl er jetzt schneller ausgeführt wird). Das selbe Prinzip kann auf die Prozedur Hello angewendet werden: Wir können die Programmausgabe durch Veränderung des Codes der Prozedur modifizieren und die Methode Button2Click wird automatisch ihr Verhalten ändern. Hier folgt, wie wir den Code verändern können:

procedure Hello;
begin
  MessageDlg ('Hallo Freunde!', mtInformation, [mbOK]);
end;

Tip: Wenn Sie eine existierende Delphi-Funktion oder -Prozedur aufrufen, oder auch jede VCL-Methode, so müssen Sie die Anzahl und den Typ der Parameter kennen. Der Delphi-Editor hilft Ihnen durch einen Hinweis der Parameterliste einer Funktion oder Prozedur, indem ein flüchtiges Hilfefenster aufklappt, sobald Sie den Namen und die öffnende Klammer eingegeben haben. Diese Eigenschaft wird als Code Parameters bezeichnet und ist Teil der Code Insight Technologie.

Referenzparameter

Pascalroutinen gestatten es, Parameter als Wert oder als Referenz zu übergeben. Das Standardverhalten ist die Übergabe der Parameter als Wert: der Wert wird auf den Stack kopiert und die Routine verwendet und verändert diese Kopie, nicht den Originalwert.

Die Übergabe eines Parameters als Referenz bedeutet, dass ihr Wert nicht auf den Stack der formalen Parameter der Routine kopiert wird (die Vermeidung einer Kopie bedeutet oft, dass das Programm schneller ausgeführt wird). Statt dessen verweist das Programm auf den Originalwert, also auch im Code der Routine. Dies gestattet der Prozedur oder Funktion, den Wert dieses Parameters zu verändern. Die Übergabe eines Parameters als Referenz wird durch das Schlüsselwort var ausgedrückt.

Diese Technik ist in den meisten Programmiersprachen verfügbar. Sie existiert nicht in C, aber sie wurde in C++ eingeführt, wo Sie das &-Symbol verwenden (Übergabe als Referenz). In Basic wird jeder Parameter, der nicht als ByVal gekennzeichnet ist, als Referenz übergeben.

Hier ist ein Beispiel zur Übergabe eines Parameters als Referenz unter Verwendung des Schlüsselworts var:

procedure DoubleTheValue (var Value: Integer);
begin
  Value := Value * 2;
end;

In desem Fall wird der Parameter sowohl zur Übergabe des Wertes and die Funktion als auch zur Rückgabe des neuen Wertes and den aufrufenden Code verwendet. Wenn Sie schreiben:

var
  X: Integer;
begin
  X := 10;
  DoubleTheValue (X);

dann wird der Wert der Variable X gleich 20, da die Funktion eine Referenz auf die ursprüngliche Speicherstelle von X verwendet und damit ihren Anfangswert modifiziert.

Die Übergabe von Parametern als Referenz ist sinnvoll für Aufzählungstypen, die alte Version von Strings und für große Records. Delphi-Objekte hingegen werden immer als Wert übergeben, da sie bereits selbst Referenzen sind. Aus diesem Grund hat die Übergabe eines Objekts als Referenz wenig Sinn (mit Ausnahme von Sonderfällen), da es einer Übergabe einer "Referenz auf eine Referenz" entspricht.

Delphi's lange Strings besitzen ein etwas abweichendes Verhalten: sie verhalten sich als Referenzen, aber wenn Sie eine der String-Variablen verändern, die auf den selben String im Speicher verweisen, so wird dieser String kopiert, bevor er aktualisiert wird. Ein langer String, der als Wertparameter übergeben wird, verhält sich nur in Hinblick auf Speicherverbrauch und Ausführungsgeschwindigkeit wie eine Referenz. Wenn Sie aber den Wert des Strings verändern, so wird der ursprüngliche Wert nicht verändert. Wenn Sie im Gegensatz dazu einen langen String als Referenz übergeben, so können sie den ursprünglichen Wert verändern.

Delphi 3 hat eine neue Parameterart eingeführt, out. Ein out-Parameter hat keinen Anfangswert und dient nur dazu einen Wert zurückzuliefern. Diese Parameter sollten nur für COM-Prozeduren und -Funktionen verwendet werden; allgemein ist es besser bei den effizienteren var-Parametern zu bleiben. Mit der Ausnahme, dass sie keinen Anfangswert besitzen, verhalten sich out-Parameter wie var-Parameter.

Konstante Parameter

Als Alternative zu Referenzparametern können Sie einen konstanten Parameter verwenden. Da Sie innerhalb einer Routine keinen neuen Wert an einen konstanten Parameter zuweisen können, ist der Compiler in der Lage die Parameterübergabe zu optimieren. Der Compiler kann eine Variante wählen, die ähnlich den Referenzparametern ist (oder eine const-Referenz in C++ Begriffen), aber das Verhalten bleibt ähnlich zu Werteparametern, da der ursprüngliche Wert nicht durch die Routine beeinflusst wird.

Tatsächlich können Sie versuchen, den folgenden (sinnlosen) Code zu übersetzen, Delphi wird eine Fehlermeldung ausgeben:

function DoubleTheValue (const Value: Integer): Integer;
begin
  Value := Value * 2;      // Compilationsfehler
  Result := Value;
end;

Offene Array-Parameter

Gegenüber C besitzten Pascal-Funktionen und -Prozeduren immer eine feste Anzahl von Parametern. Es gibt jedoch einen Weg, um eine variable Anzahl von Parametern an eine Routine zu übergeben, die Verwendung einen offenen Arrays.

Die Basisdefinition eines offenen Array-Parameters besteht in einem typisierten offenen Array. Das bedeutet, sie geben den Typ des Parameters an aber wissen nicht, wie viele Elemente dieses Typs das Array haben wird. Hier ist ein Beispiel für solch eine Definition:

function Sum (const A: array of Integer): Integer;
var
  I: Integer;
begin
  Result := 0;
  for I := Low(A) to High(A) do
    Result := Result + A[I];
end;

Durch die Verwendung von High(A) können wir die Größe des Arrays ermitteln. Beachten Sie auch die Verwendung des Rückgabewertes Result der Funktion, um temporäre Werte zu speichern. Sie können diese Funktion durch Angabe eines Array von Integer-Ausdrücken aufrufen:

X := Sum ([10, Y, 27*I]);

Wenn Sie ein bereits existierendes Integer-Array von beliebiger Größe haben, so können Sie es direkt an eine Routine übergeben, die einen offenen Array-Parameter benötigt, oder statt dessen können Sie die Slice-Funktion aufrufen, um nur einen Teil des Arrays zu übergeben (der durch den zweiten Parameter angegeben wird). Hier ist ein Beispiel, wo ein komplettes Array als Parameter übergeben wird:

var
  List: array [1..10] of Integer;
  X, I: Integer;
begin
  // das Array initialisieren
  for I := Low (List) to High (List) do
    List [I] := I * 2;
  // aufrufen
  X := Sum (List);

Wenn Sie nur einen Teil des Arrays über die Slice-Funktion übergeben möchten, so verwenden Sie diesen Weg:

X := Sum (Slice (List, 5));

Sie können alle Code-Fragmente aus diesem Abschnitt im Beispiel OpenArr finden (vergl. Abbildung 6.1 weiter unten für das zugehörige Formular).

Abbildung 6.1: Das Beispiel OpenArr, nachdem der Schalter Partial Slice gedrückt wurde.

Typisierte offene Arrays sind in Delphi 4 voll kompatibel mit dynamischen Arrays (die in Delphi 4 eingeführt wurden und im Kapitel 8 behandelt werden). Dynamische Arrays verwenden die selbe Syntax wie offene Arrays, mit dem Unterschied, dass Sie eine Notation wie array of Integer zur Deklaration einer Variablen verwenden können und nicht nur, um einen Parameter zu übergeben.

Typvariable offene Array-Parameter

Neben den typisierten offenen Arrays gestattet Delphi Ihnen auch die Defintion von typvariablen oder untypisierten offenen Arrays. Diese besondere Array-Art besitzt eine undefinierte Anzahl von Werten, die nützlich für die Übergabe von Parametern sein kann.

Technisch gestattet Ihnen der Konstrukt array of const die Übergabe eines Arrays mit einer undefinierten Anzahl von Elementen unterschiedlichen Typs an eine Routine. Hier ist z.B. die Definition der Format-Funktion (wir werden die Anwendung dieser Funktion noch im Kapitel 7 bei der Behandlung von Strings sehen):

function Format (const Format: string;
  const Args: array of const): string;

Der zweite Parameter ist ein offenes Array, welches eine unbestimmte Anzahl von Werten aufnhemen kann. Damit können Sie diese Funktion auf die folgende Weise aufrufen:

N := 20;
S := 'Total:';
Label1.Caption := Format ('Total: %d', [N]);
Label2.Caption := Format ('Int: %d, Float: %f', [N, 12.4]);
Label3.Caption := Format ('%s %d', [S, N * 2]);

Beachten Sie, dass Sie Parameter sowohl als konstante Werte, als Werte einer Variable oder als Ausdruck übergeben können. Die Deklaration einer derartigen Funktion ist einfach, aber wie wird sie codiert? Woher weiß man den Typ der Parameter? Die Werte eines typvarianten offenen Array-Parameters sind kompatibel mit den Elementen des TVarRec-Typs.

Hinweis: Verwechseln Sie nicht den TVarRec-Record mit dem TVarData-Record, der durch den Variant-Typ selbst verwendet wird. Diese zwei Strukturen haben eine unterschiedliche Bedeutung und sind nicht kompatibel. Auch die Liste der möglichen Typen ist unterschiedlich, da TVarRec Delphi Datentypen beinhaltet, während TVarData OLE Datentypen enthält.

Der TVarRec-Record besitzt die folgenden Struktur:

type
  TVarRec = record
    case Byte of
      vtInteger:    (VInteger: Integer; VType: Byte);
      vtBoolean:    (VBoolean: Boolean);
      vtChar:       (VChar: Char);
      vtExtended:   (VExtended: PExtended);
      vtString:     (VString: PShortString);
      vtPointer:    (VPointer: Pointer);
      vtPChar:      (VPChar: PChar);
      vtObject:     (VObject: TObject);
      vtClass:      (VClass: TClass);
      vtWideChar:   (VWideChar: WideChar);
      vtPWideChar:  (VPWideChar: PWideChar);
      vtAnsiString: (VAnsiString: Pointer);
      vtCurrency:   (VCurrency: PCurrency);
      vtVariant:    (VVariant: PVariant);
      vtInterface:  (VInterface: Pointer);
  end;

Jeder mögliche Record besitzt das VType-Feld, obwohl das nicht auf den ersten Blick zu erkennen ist, da es nur einmal deklariert wird, sowie die eigentlichen Daten in Integer-Größe (generell als Referenz oder als Pointer).

Unter Anwendung dieser Informationen können wir jetzt eine Funktion schreiben, die in der Lage ist, unterschiedliche Datentypen zu bearbeiten. In der Beispielfunktion SumAll wollte ich die Möglichkeit besitzen, unterschiedliche Datentypen zu addieren. Strings werden dabei in Integer umgewandelt, Zeichen in ihren korrespondierenden Aufzählungswert und 1 wird für boolsche Werte addiert, deren Wert True ist. Der Code basiert auf einer Fallverzweigung und ist ganz einfach, obwohl wir häufig Zeiger dereferenzieren müssen:

function SumAll (const Args: array of const): Extended;
var
  I: Integer;
begin
  Result := 0;
  for I := Low(Args) to High (Args) do
    case Args [I].VType of
      vtInteger: Result :=
        Result + Args [I].VInteger;
      vtBoolean:
        if Args [I].VBoolean then
          Result := Result + 1;
      vtChar:
        Result := Result + Ord (Args [I].VChar);
      vtExtended:
        Result := Result + Args [I].VExtended^;
      vtString, vtAnsiString:
        Result := Result + StrToIntDef ((Args [I].VString^), 0);
      vtWideChar:
        Result := Result + Ord (Args [I].VWideChar);
      vtCurrency:
        Result := Result + Args [I].VCurrency^;
    end; // case
end;

Diesen Code habe ich zum Beispiel OpenArr hinzugefügt, welches die Funktion SumAll aufruft, wenn der entsprechede Schalter betätigt wird:

procedure TForm1.Button4Click(Sender: TObject);
var
  X: Extended;
  Y: Integer;
begin
  Y := 10;
  X := SumAll ([Y * Y, 'k', True, 10.34, '99999']);
  ShowMessage (Format (
    'SumAll ([Y*Y, ''k'', True, 10.34, ''99999'']) => %n', [X]));
end;

In Abbildung 6.2 können Sie die Ausgabe dieses Aufrufs zusammen mit dem Formular vom Beispiel OpenArr sehen.

Abbildung 6.2: Das Formular des Beispiels OpenArr mit der Message-Box, die angezeigt wird, wenn der Schalter UnTyped gedrückt wurde.

Delphi's Aufrufkonventionen

Die 32-Bit Version von Delphi hat eine neue Methode zur Übergabe von Parametern eingeführt, die unter dem Begriff fastcall (schnelle Aufrufkonvention) bekannt ist: Wann immer möglich, werden bis zu drei Parametern in CPU-Registern übergeben, wodurch die Funktion viel schneller wird. Die schnelle Aufrufkonvention (in Delphi 3 als Standardeinstellung verwendet) wird durch das Schlüsselwort register gekennzeichnet.

Das Problem besteht darin, dass diese die Standardkonvention ist und Funktionen, die sie verwenden, nicht kompatibel mit Windows sind: Die Funktionen des Win32-API müssen unter Verwendung der Aufrufkonvention stdcall deklariert werden, eine Mischung aus der ursprünglichen Pascal Aufrufkonvention für das Win16-API und der Aufrufkonvention cdecl der Sprache C.

Das ist jedoch generell kein Grund, diese neue, schnelle Aufrufkonvention nicht zu verwenden, es sei denn, Sie erstellen externe Windows-Aufrufe oder definieren Windows Callback-Funktionen. Wir werden am Ende dieses Kapitels noch ein Beispiel zur Verwendung der Aufrufkonvention stdcall sehen. Sie können eine Zusammenfassung von Delphi's Aufrufkonventionen unter dem Thema Aufrufkonventionen in der Delphi-Hilfe finden.

Was ist eine Methode?

Wenn Sie bereits mit Delphi gearbeitet haben oder beim Lesen dieses Handbuchs, so werden Sie wahrscheinlich schon den Begriff Methode gehört haben. Eine Methode ist eine besondere Art von Funktion oder Prozedur, die mit einem Datentyp verbunden ist, einer Klasse. In Delphi müssen wir jedes mal, wenn wir ein Ereignis behandeln, eine Methode definieren, im allgemeinen eine Prozedur. Generell wird der Begriff Methode verwendet, um Funktionen und Prozeduren, die zu einer Klasse gehören zu kennzeichnen.

Wir haben bereits eine Reihe von Methoden in den Beispielen zu diesem und den vorherigen Kapiteln gesehen. Hier ist eine leere Methode die automatisch von Delphi in den Quellcode eingefügt wurde:

procedure TForm1.Button1Click(Sender: TObject);
begin
  { hier folgt der Code }
end;

Vorwärtsdeklarationen

Wenn Sie einen Bezeichner (beliebiger Art) verwenden möchten, so muß der Compiler zuvor bereits eine Art von Deklaration gesehen haben, um zu wissen auf was sich der Bezeichner bezieht. Aus diesem Grund geben Sie gewöhnlich eine vollständige Vereinbarung der Routine an bevor Sie sie verwenden. Es gibt jedoch Fälle, wo das nicht möglich ist. Wenn Prozedur A die Prozedur B aufruft und Prozedur B seinerseits Prozedur A aufruft und Sie beginnen, den Code zu schreiben, so werden Sie eine Routine aufrufen müssen, für die der Compiler noch keine Deklaration gesehen hat.

Wenn Sie die Existenz einer Prozedur oder Funktion mit einem bestimmten Namen und den erforderlichen Parametern deklarieren möchten, ohne den eigentlichen Code mitzuliefern, so können Sie die Prozedur oder Funktion gefolgt von dem Schlüsselwort forward schreiben:

procedure Hello; forward;

Später dann muß eine vollständige Definition des Code der Prozedur nachgeliefert werden, sie jedoch vor ihrer vollständigen Definition aufgerufen werden. Hier ist ein verrücktes Beispiel, nur dazu gedacht, Ihnen die Idee zu demonstrieren:

procedure DoubleHello; forward;

procedure Hello;
begin
  if MessageDlg ('Möchten Sie eine doppelte Meldung?',
      mtConfirmation, [mbYes, mbNo], 0) = mrYes then
    DoubleHello
  else
    ShowMessage ('Hallo');
end;

procedure DoubleHello;
begin
  Hello;
  Hello;
end;

Dieser Ansatz gestattet Ihnen, eine wechselseitige Rekursion zu schreiben: DoubleHello ruft Hello auf, aber Hello kann auch DoubleHello aufrufen. Natürlich muss eine Bedingung zum Abbruch der Rekursion existieren, um einen Stack-Überlauf zu vermeiden. Sie können diesen Code mit einigen kleinen Änderungen im Beispiel DoubleH finden.

Obwohl eine Vorwärtsdeklaration für Prozeduren in Delphi nicht sehr oft vorkommt, gibt es einen ähnlichen Fall, der viel häufiger auftritt. Wenn Sie eine Prozedur oder Funktion im Interface-Abschnitt einer Unit deklarieren (mehr über Units im Kapitel 11), so wird sie als Vorwärtsdeklaration betrachtet, auch wenn kein forward-Schlüsselwort vorhanden ist. Tatsächlich können Sie den Körper einer Routine nicht im Interface-Abschnitt einer Unit schreiben. Gleichzeitig müssen Sie in der selben Unit die eigentliche Implementation für jede Routine, die Sie deklariert haben, bereitstellen.

Das gleiche gilt für die Deklaration der Methode innerhalb eines Klassen-Typs, der automatisch von Delphi erzeugt wurde (wenn Sie ein Ereignis eines Formulars oder einer seiner Komponenten hinzufügen). Die Ereignisbehandlungsroutinen, die innerhalb der TForm-Klasse deklariert werden, sind Vorwärtsdeklarationen: Der Code wird im Implementation-Abschnitt der Unit bereitgestellt. Hier ist ein Ausschnitt aus dem Quellcode eines früheren Beispiels mit der Deklaration einer Button1Click-Methode:

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

Prozedurale Typen

Eine weitere besondere Eigenschaft von Object Pascal ist das Vorhandensein von prozeduralen Typen. Diese sind wirklich ein fortgeschrittenes Thema der Sprache, welche nur wenige Delphi-Programmierer regelmäßig verwenden. Da wir jedoch damit in Zusammenhang stehende Themen in den weiteren Kapiteln diskutieren werden (besonders Methodenzeiger, eine Technik die von Delphi intensiv genutzt wird), ist es Wert einen kurzen Blick auf sie zu werfen. Wenn Sie Anfänger in der Programmierung sind, so können Sie diesen Abschnitt vorerst überspringen und auf ihn zurückkommen, wenn Sie sich dazu bereit fühlen.

In Pascal existiert das Konzept der prozeduralen Typen (welches dem Sprachkonzept der Funktionszeiger von C ähnelt). Die Deklaration eines prozeduralen Typs gibt die Parameterliste an und eventuell den Rückgabetyp im Falle einer Funktion. Beispielsweise können Sie den Typ einer Prozedur mit einem Integer-Parameter, der als Referenz übergeben wird, wir folgt deklarieren:

type
  IntProc = procedure (var Num: Integer);

Dieser prozedurale Typ ist mit jeder Routine kompatibel, die exakt die selben Parameter besitzt (oder die selbe Funktionssignatur, um C-Jargon zu verwenden). Hier ist ein Beispiel für eine kompatible Routine:

procedure DoubleTheValue (var Value: Integer);
begin
  Value := Value * 2;
end;

Hinweis: In der 16-Bit Version von Delphi müssen Routinen unter Verwendung der far-Direktive deklariert werden, um sie als aktuelle Werte eines prozeduralen Typs verwenden zu können.

Prozedurale Typen können für unterschiedliche Zwecke verwendet werden: Sie können Variablen eines prozeduralen Typs deklarieren, oder einen prozeduralen Type -einen Funktionszeiger- als Parameter an eine andere Routine übergeben. Die vorhergehenden Typ- und Prozedur-Deklarationen vorausgesetzt, können Sie folgenden Code schreiben:

var
  IP: IntProc;
  X: Integer;
begin
  IP := DoubleTheValue;
  X := 5;
  IP (X);
end;

Dieser Code hat den selben Effekt wie die folgende, kürzere Version:

var
  X: Integer;
begin
  X := 5;
  DoubleTheValue (X);
end;

Die erste Version ist deutlich komplexer, warum sollten wir sie daher verwenden? In einigen Fällen kann es nützlich sein, vorher zu entscheiden welche Funktion aufzurufen ist und den eigentlichen Aufruf später auszuführen. Es wäre möglich, ein komplexes Beispiel aufzubauen, das diesen Ansatz zeigt. Ich ziehe es jedoch vor, Sie ein ziemlich einfaches Beispiel namens ProcType untersuchen zu lassen. Dieses Beispiel ist komplexer als die, welche wir bisher gesehen haben, um die Situation etwas realistischer zu gestalten.

Erzeugen Sie einfach ein leeres Projekt und plazieren Sie zwei Auswahlschalter und einen Schalter, wie in Abbildung 6.3 dargestellt. Dieses Beispiel basiert auf zwei Prozeduren. Eine Prozedur wird verwendet, um den Wert des Parameters zu verdoppeln. Diese Prozedur ähnelt der Version, die ich bereits in diesem Abschnitt gezeigt habe. Eine zweite Prozedur wird verwendet, um den Wert des Parameters zu verdreifachen, sie nennt sich daher TripleTheValue:

Abbildung 6.3: Das Formular des Beispiels ProcType.

procedure TripleTheValue (var Value: Integer);
begin
  Value := Value * 3;
  ShowMessage ('Dreifacher Wert: ' + IntToStr (Value));
end;

Beide Prozeduren zeigen an, was passiert, damit wir wissen, dass sie aufgerufen wurden. Dies ist eine einfache Debug-Möglichkeit, die Sie verwenden können, um festzustellen, ob und wann ein bestimmter Abschnitt des Codes ausgeführt wird, anstatt Haltepunkte einzufügen.

Jedes mal, wenn der Anwender den Apply-Schalter betätigt wird eine der zwei Prozeduren in Abhängigkeit des Zustands der Auswahlschalter ausgeführt. Tatsächlich ist es so, wenn Sie zwei Auswahlschalter auf einem Formular haben, dass zu einem beliebigen Zeitpunkt immer nur einer der Beiden ausgewählt sein kann. Dieser Code hätte dadurch implementiert werden können, dass der Wert der Auswahlschalter im Code des OnClick-Ereignisses von Schalter Apply ausgewertet wird. Um jedoch die Verwendung von prozeduralen Typen zu demonstrieren habe ich einen längeren aber dafür interessanteren Ansatz gewählt. Jedes mal, wenn der Anwender auf einen der Auswahlschalter klickt wird eine der Prozeduren in einer Variable gespeichert:

procedure TForm1.DoubleRadioButtonClick(Sender: TObject);
begin
  IP := DoubleTheValue;
end;

Wenn der Anwender auf den Schalter drückt, wird die Prozedur ausgeführt, die wir gespeichert haben:

procedure TForm1.ApplyButtonClick(Sender: TObject);
begin
  IP (X);
end;

Um den drei unterschiedlichen Funktionen den Zugriff auf die Variablen IP und X zu ermöglichen, müssen wir sie im gesamten Formular sichtbar machen; sie können nicht lokal deklariert werden (d.h. innerhalb einer der Methoden). Eine Lösung für dieses Problem ist es, die Variablen innerhalb der Formulardeklaration zu platzieren:

type
  TForm1 = class(TForm)
    ...
  private
    { Private declarations }
    IP: IntProc;
    X: Integer;
  end;

Wir werden im folgenden Kapitel sehen, was das genau bedeutet, aber im Moment sollten Sie den von Delphi für den Class-Typ erzeugten Code wie oben gezeigt modifizieren und die Definitionen der prozeduralen Typen hinzufügen, wie ich es oben dargestellt habe. Um diese zwei Variablen mit passenden Werten zu initialisieren, können wir das OnCreate-Ereignis des Formulars verwenden (selektieren Sie dieses Ereignis im Objektinspektor nachdem Sie das Formular aktiviert haben, oder doppelklicken Sie einfach das Formular). Ich empfehle Ihnen, das entsprechende Listing zu vergleichen, um die Details des Quellcodes zu diesem Beispiel zu studieren.

Im Kapitel 9 finden Sie ein praktisches Beispiel zur Verwendung von prozeduralen Typen im Abschnitt Eine Windows Callback-Funktion.

newÜberladene Routinen

Die Idee der Überladung ist einfach: Der Compiler gestattet Ihnen, zwei oder mehr Funktionen oder Prozeduren unter Verwendung des selben Namens, vorausgesetzt, dass die Parameter unterschiedlich sind. Der Compiler kann seinerseits durch Prüfung der Parameter feststellen, welche der Routinenversionen Sie aufrufen möchten.

Betrachten Sie die Serie von Funktionen, die aus der VCL-Unit Math entnommen ist:

function Min (A,B: Integer): Integer; overload;
function Min (A,B: Int64): Int64; overload;
function Min (A,B: Single): Single; overload;
function Min (A,B: Double): Double; overload;
function Min (A,B: Extended): Extended; overload;

Wenn Sie Min(10, 20) aufrufen, so kann der Compiler leicht feststellen, dass Sie die erste Funktion der Gruppe aufrufen, damit wird der Rückgabewert vom Typ Integer sein.

Es gibt zwei Grundregeln:

Hier sind drei überladene Versionen der Prozedur ShowMsg. Ich habe sie zu dem Beispiel OverDef hinzugefügt (eine Applikation, die Überladung und Vorgabeparameter demonstriert):

procedure ShowMsg (str: string); overload;
begin
  MessageDlg (str, mtInformation, [mbOK], 0);
end;

procedure ShowMsg (FormatStr: string;
  Params: array of const); overload;
begin
  MessageDlg (Format (FormatStr, Params),
    mtInformation, [mbOK], 0);
end;

procedure ShowMsg (I: Integer; Str: string); overload;
begin
  ShowMsg (IntToStr (I) + ' ' + Str);
end;

Die drei Funktionen zeigen ein Nachrichtenfenster mit einem Text an, teilweise nach einer Formatierung des Strings auf unterschiedliche Weise. Hier sind die drei Aufrufe aus dem Programm:

ShowMsg ('Hallo');
ShowMsg ('Gesamtsumme = %d.', [100]);
ShowMsg (10, 'MBytes');

Was mich auf positive Weise überrascht hat, ist die Tatsache, das Delphi's Code Parameter Technologie sehr schön mit überladenen Prozeduren und Funktionen zusammenarbeitet. Wenn Sie die öffnende Klammer nach dem Namen der Routine eingegen werden alle verfügbaren Alternativen aufgelistet. Wenn Sie die Parameter eingeben verwendet Delphi ihren Typ, um zu bestimmen, welche der Alternativen noch verfügbar sind. In Abbildung 6.4 können Sie sehen, dass Delphi nach Eingabe eines konstanten Strings nur die kompatiblen Versionen anzeigt (und die Version der ShowMsg-Prozedur unterdrückt, die einen Integer als ersten Parameter erfordert).

Abbildung 6.4: Die unterschiedlichen Alternativen die als Code-Parameter für überladene Routinen verfügbar sind, werden entsprechend den bereits angegebenen Parametern gefiltert.

Die Tatsache, dass jede Version einer überladenen Routine korrekt gekennzeichnet sein muß bedeutet, dass Sie keine existierenden Routinen der selben Unit überladen können, die nicht durch das Schlüsselwort overload gekennzeichnet ist. (Die Fehlermeldung die sie erhalten, wenn sie es dennoch versuchen, lautet: Bei der vorherigen Deklaration von <name> wurde die Direktive 'overload' nicht angegeben.) Sie können jedoch eine Routine überladen, die ursprünglich in einer anderen Unit deklariert wurde. Dies dient der Kompatibilität mit älteren Versionen von Delphi, die es unterschiedlichen Units gestattet haben, den selben Routinennamen wiederzuverwenden. Beachten Sie jedoch, dass dieser besondere Fall keine zusätzliche Eigenschaft der Überladung darstellt, sondern nur die Probleme aufzeigt, mit denen Sie konfrontiert werden können.

Beispielswiese können Sie folgenden Code zu einer Unit hinzufügen:

procedure MessageDlg (str: string); overload;
begin
  Dialogs.MessageDlg (str, mtInformation, [mbOK], 0);
end;

Dieser Code überlädt die ursprüngliche Routine MessageDlg nicht wirklich, denn wenn Sie schreiben:

MessageDlg ('Hello');

erhalten Sie eine hübsche Fehlermeldung, die anzeigt, dass einer der Parameter fehlt. Der einzige Weg, die lokale Version anstatt der VCL-Version aufzurufen, ist es sich ausdrücklich auf die lokale Unit zu beziehen, etwas was die Idee des Überladens vereitelt:

OverDefF.MessageDlg ('Hello');

newStandardparameter

Eine verwandte neue Eigenschaft von Delphi 4 besteht darin, dass Sie einen Standardwert für die Parameter einer Routine angeben können. Sie können die Routine dann mit oder ohne diesen Parameter aufrufen. Lassen Sie mich ein Beispiel zeigen. Wir können die folgende Kapselung der Methode MessageBox des globalen Objekts Application definieren, welche PChar anstatt von String verwendet, indem wir zwei Standardparameter angeben:

procedure MessBox (Msg: string;
  Caption: string = 'Warning';
  Flags: LongInt = mb_OK or mb_IconHand);
begin
  Application.MessageBox (PChar (Msg),
    PChar (Caption), Flags);
end;

Mit dieser Definition können wir die Prozedur auf jede der folgenden Arten aufrufen:

MessBox ('Etwas hier ist falsch!');
MessBox ('Etwas hier ist falsch!', 'Achtung');
MessBox ('Hallo', 'Meldung', mb_OK);

In Abbildung 6.5 können Sie sehen, dass Delphi's Code Parameter einen unterschiedlichen Stil verwendet, um Parameter, die einen Standardwert besitzen, korrekt anzuzeigen. Damit können Sie leicht feststellen, welche Parameter ausgelassen werden können.

Abbildung 6.5: Delphi's Code Parameter markiert Parameter, die einen Standardwert besitzen, mit eckigen Klammern; Sie können diese beim Aufruf auslassen.

Beachten Sie, dass Delphi keinen speziellen Code erzeugt, um Standardparameter zu unterstützen; es erzeugt auch keine mehrfachen Kopien der Routinen. Die fehlenden Parameter werden einfach durch den Compiler zu dem aufrufenden Code hinzugefügt.

Es gibt eine wichtige Einschränkung, die die Verwendung von Standardparametern betrifft: Sie können keine Parameter "überspringen". Beispielsweise können Sie nicht den dritten Parameter an die Funktion übergeben, nachdem Sie den zweiten ausgelassen haben:

MessBox ('Hallo', mb_OK); // Fehler

Dies ist die Hauptregel für Standardparameter: In einem Aufruf können Sie nur die Parameter auslassen, die von hinten beginnen. Mit anderen Worten, wenn Sie einen Parameter auslassen, so müssen Sie auch alle folgenden auslassen.

Es existieren jedoch noch einige weitere Regeln für Standardparameter:

Die gleichzeitige Verwendung von Standardparametern und Überladung kann einige Probleme mit sich bringen, da die beiden Eigenschaften in Konflikt zueinander stehen können. Wenn ich beispielsweise die folgende neue Version der ShowMsg-Prozedur zum vorherigen Beispiel hinzufüge:

procedure ShowMsg (Str: string; I: Integer = 0); overload;
begin
  MessageDlg (Str + ': ' + IntToStr (I),
    mtInformation, [mbOK], 0);
end;

dann wird der Compiler sich nicht beschweren - es ist eine zulässige Definition. Jedoch wird der Aufruf:

ShowMsg ('Hello');

durch den Compiler als Fehler Doppeldeutiger überladener Aufruf von 'ShowMsg' markiert. Beachten Sie, dass dieser Fehler in einer Code-Zeile angezeigt wird, die vor der neuen Definition der Überladung korrekt übersetzt wurde. Praktisch haben wir keine Möglichkeit, die Prozedur ShowMsg mit nur einem Parameter aufzurufen, da der Compiler nicht weiß, ob wir die Version mit dem einzelnen String-Parameter oder die mit dem String-Parameter und dem Integer-Parameter mit Standardwert aufrufen wollen. Wenn er einen derartigen Zweifel hat, so hält der Compiler an und fordert den Programmierer auf, seine oder ihre Absichten klarer zu formulieren.

Zusammenfassung

Das Schreiben von Prozeduren und Funktionen ist ein Schlüsselelement der Programmierung, obwohl Sie in Delphi dazu tendieren werden, Methoden zu schreiben -- Prozeduren und Funktionen, die mit Klassen und Objekten verbunden sind.

Anstatt jedoch zu den objektorientierten Eigenschaften überzugehen, werden die nächsten Kapitel Ihnen einige Details weiterer Programmierelemente in Pascal erläutern, beginnend mit Strings.

Nächstes Kapitel: Strings verwenden

© Copyright Marco Cantù, Wintech Italia Srl 1995-2000
© Copyright der deutschen Übersetzung Immo Wache, 2000