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.
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.
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:
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 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).