Deklaration von Funktionsparametern

Wie Ihnen sicher bekannt sein dürfte kann man an Prozeduren und Funktionen Parameter (auch Argumente genannt) übergeben. Was nach meiner Erfahrung allerdings oftmals für Verwirrung sorgt ist die Tatsache, dass dies auf verschiedene Arten geschehen kann. Im schlimmsten Fall entscheidet die Art der Übergabe, ob das gewünschte Ergebnis erreicht wird oder nicht.

Generell gibt es in Delphi 2 verschiedene Arten von Parametern:

  • Call by Reference
  • Call by Value

Call by Reference bedeutet, dass die zu bearbeitende Variable im Original übergeben wird. Bei einem Call by Value hingegen wird eine Kopie erzeugt, mit dem Wert des Originals belegt und übergeben. Das dauert logischerweise einen Moment, weshalb Calls by Reference in vielen Fällen schneller sind. Schauen wir uns die verschiedenen Möglichkeiten einmal an.

Parametertypen in Delphi:

  • Wertparameter
  • Variablenparameter (Schlüsselwort var)
  • Konstantenparameter (Schlüsselwort const)
  • Ausgabeparameter (Schlüsselwort out)
Wie der Name schon vermuten lässt handelt es sich bei einem Wertparameter um einen Call by Value. Alle anderen benutzen Referenzen.

Wie wirkt sich das aus?

Nehmen wir einmal ein Beispiel: viele Sortieralgorithmen arbeiten mit einem Wertetausch. Schreiben wir also flugs eine Routine zum Tauschen zweier Ganzzahlen (Dreieckstausch):
procedure SwapValues(a, b: integer);
var
  tmp: integer;
begin
  tmp := a;
  a := b;
  b := tmp;
end;
Wir testen das Ganze:
procedure TForm1.Button1Click(Sender: TObject);
var
  Zahl1, Zahl2: integer;
begin
  Zahl1 := 1;
  Zahl2 := 2;
  SwapValues(Zahl1, Zahl2);
  ShowMessage(Format('Zahl1: %d, Zahl2: %d', [Zahl1, Zahl2]));
end;
Nanu? Wir müssen feststellen, dass Zahl1 immer noch 1 und Zahl2 immer noch 2 ist statt umgekehrt. Schuld daran ist die falsche Parameterübergabe. a und b werden als Kopien an SwapValues übergeben und sind nur innerhalb dieser Prozedur gültig. Das bedeutet, dass zwar a und b brav ihre Werte tauschen, nicht aber Zahl1 und Zahl2, da es sich jeweils um verschiedene Speicherbereiche handelt. Wir müssen also aus den Werteparametern Variablenparameter machen:
procedure SwapValues(var a, b: integer);
var
  tmp: integer;
begin
  tmp := a;
  a := b;
  b := tmp;
end;
Wie Sie feststellen werden beheben 3 kleine Buchstaben (nämlich das Wörtchen "var") unser Problem.

Unterschiede zwischen den einzelnen Typen

Wertparameter
Ein Wertparameter kann innerhalb der Routine geändert werden, ohne dass das Einfluss auf den Originalwert hat. Anwendungsbeispiel:
procedure GetFilesInDirectory(Path: string; List: TStrings);
begin
  Path := IncludeTrailingPathDelimiter(Path);
  //...
end;
Hier wird sichergestellt, dass der übergebene Pfad innerhalb der Routine mit einem Backslash endet. Da der Originalwert nicht betroffen ist können Sie auch Konstanten übergeben.
Variablenparameter
Ein Variablenparameter wird dann verwendet, wenn ein Wert übergeben und bearbeitet zurückkommen soll (siehe obiges Tauschbeispiel). Da der Wert zurückgeschrieben wird ist es nicht möglich, Konstanten zu übergeben.
Konstantenparameter
Konstantenparameter sollten dann zur Anwendung kommen, wenn der Wert innerhalb der Routine nicht bearbeitet werden darf bzw. soll. Ist der Datentyp des Parameters größer als ein Zeiger, kann ein Konstantenparameter im Vergleich zum Wertparameter die Performance steigern.
function AllToLower(const s: string): string;
begin
  Result := AnsiLowerCase(s);
end;
Sie können natürlich, wie der Name bereits vermuten lässt, Konstanten übergeben.
Ausgabeparameter
Mit Ausgabeparametern verhält es sich ähnlich wie mit Variablenparametern. Allerdings wird der Parameter innerhalb der Routine nirgends ausgewertet, sondern lediglich gesetzt.
procedure Add(a, b: integer; out ResultValue: integer);
begin
  ResultValue := a + b;
end;
Da der Ausgabewert in den Speicherbereich des Originals geschrieben wird ist es nicht möglich, Konstanten zu übergeben.

Vorsicht bei Zeigertypen!

In den allermeisten Fällen deutet ein Zeigertyp (wozu übrigens auch Objektinstanzen gehören) als Variablenparameter auf einen Denkfehler hin. Im Normalfall werden Sie die dahinter liegenden Daten ändern wollen, aber nicht den Zeiger selbst (auch hier bestätigen Ausnahmen die Regel, aber es sind eben Ausnahmen). Schauen Sie sich bitte einmal folgendes Negativbeispiel an (bitte nicht nachmachen):
procedure ListNumbers(var List: TStrings);
var
  SL: TStrings;
  i: integer;
begin
  SL := TStringlist.Create;
  try
    for i := 1 to 10 do
      SL.Add(Format('%.2d', [i]));
    List := SL;
  finally
    SL.Free;
  end;
end;
Nun rufen wir diese Routine einmal auf.
procedure TForm1.Button1Click(Sender: TObject);
var
  Stringlist: TStrings;
begin
  Stringlist := TStringlist.Create;
  try
    ListNumbers(Stringlist);
    ShowMessage(IntToStr(Stringlist.Count));
  finally
    Stringlist.Free;
  end;
end;
Das Ende vom Lied: Count ist 0, anschließend ernten wir eine ungültige Zeigeroperation und ein Speicherleck. Woran liegt das?
Nun, die aufrufende Methode ist vollkommen in Ordnung, der Fehler steckt in "ListNumbers". Ihr wird eine TStrings-Instanz übergeben, sie selbst legt auch eine an, befüllt diese und weist sie dann der übergebenen zu. In diesem Moment zeigen also sowohl die lokale Variable als auch der Parameter auf dieselbe Instanz. Diese wird dann freigegeben, so dass der Parameter ins Nirwana zeigt. Der abschließende Aufruf von Free macht dann das Chaos perfekt: "Stringlist" ist zwar nicht mehr da, aber auch nicht nil, wir haben uns also einen wilden Zeiger gebaut.
Zum Vergleich hier eine bessere Variante:
procedure ListNumbers(const List: TStrings);
var
  i: integer;
begin
  (* Sicherheitshalber auf nil prüfen *)
  Assert(Assigned(List));
  List.BeginUpdate;
  try
    for i := 1 to 10 do
      List.Add(Format('%.2d', [i]));
  finally
    List.EndUpdate;
  end;
end;
Zum Abschluss noch eine der erwähnten Ausnahmen: angenommen, Sie verwalten eine Liste von Strukturen (Records). Wie bereits eingangs erwähnt arbeiten viele Sortieralgorithmen mit Wertetausch. Da es nun aber sehr viel performanter ist, 2 Zeiger (4 Byte unter 32Bit) auszutauschen als die kompletten Strukturen, arbeiten Sie mit Zeigern auf die Strukturen.
type
  PPerson = ^TPerson;
  TPerson = record
    Name,
    Vorname: Shortstring;
    Geburtsdatum: TDateTime;
  end;
Wenn Sie nun also 2 PPerson austauschen, bewegen Sie jeweils 4 Byte hin und her. Bei TPerson hingegen wären das jeweils 520 Byte. Also tauschen Sie so
procedure SwapPersons(var Person1, Person2: PPerson);
var
  tmp: PPerson;
begin
  tmp := Person1;
  Person1 := Person2;
  Person2 := tmp;
end;
Hier sind die Variabenparameter richtig und gerechtfertigt, da ja tatsächlich die Zeiger gemeint sind und nicht die Daten (wenn Sie mit Ihrem Ehegatten die Autos tauschen, schleppen Sie ja auch nicht die Kfz, sondern tauschen die Schlüssel aus).