Objektinstanzen als Funktionsrückgabe

Vielleicht haben Sie auch schon Funktionen geschrieben, die eine Objektinstanz zurückgeben. So etwas könnte z.B. so aussehen:

function TMyClass.GetNames: TStrings;
var
  List: TStringlist;
begin
  List := TStringlist.Create;
  //Liste beispielhaft befüllen
  List.Add('Heinz');
  List.Add('Hans');
  List.Add('August');
  Result := List;
end;

Mein persönlicher Rat lautet: Tun Sie das nicht!

Was ist daran schlecht?

Es lauert immer unterschwellig die Gefahr, Speicherlecks zu produzieren. Das krasseste Beispiel, das mir bislang in den Foren untergekommen ist, ging ungefähr so (die aufgerufene Methode soll die oben gezeigte sein):

procedure TMyClass.VerarbeiteNames; 
var
  i: integer; 
begin 
  for i := 0 to GetNames.Count - 1 do 
    Stringverarbeitung(GetNames[i]); 
end;

Das ist syntaktisch vollkommen in Ordnung, aber raten Sie einmal, wieviele TStrings-Instanzen in diesem Stückchen Code angelegt werden. Falls Sie es nicht wissen: es sind Listenanzahl + 1 Instanzen, welche nirgends freigegeben werden (können). Um Letzteres zu gewährleisten, hätte die Methode so aussehen müssen:

procedure TMyClass.VerarbeiteNames;
var
  List: TStringlist;
  i: integer;
begin 
  List := GetNames;
  try
    for i := 0 to List.Count - 1 do
      Stringverarbeitung(List[i]);
  finally
    List.Free;
  end; 
end;

Das ist schon besser, hat aber immer noch einen Nachteil: die Freigabe erfolgt auf einer anderen Ebene als die Instanzierung. Es ist somit nicht auf den ersten Blick zu erkennen, was da im finally eigentlich freigegeben wird, da ja nirgends ein sichtbarer Konstruktor-Aufruf erfolgt ist. Den würde man wohl erst beim schrittweisen Ausführen im Debugger ausfindig machen können.

Wie anders (=besser) machen?

Machen Sie aus der Funktion eine Prozedur, welche die zu bearbeitende Instanz als Parameter entgegennimmt.

procedure TMyClass.GetNames(List: TStrings);
begin
  Assert(Assigned(List));
  List.BeginUpdate;
  try
    List.Add('Heinz');
    List.Add('Hans');
    List.Add('August');
  finally
    List.EndUpdate;
  end; 
end;

Damit erreichen Sie, dass die Instanz außerhalb angelegt werden muss, Sie verlagern somit die Ebene.

procedure TMyClass.VerarbeiteNames;
var
  i: integer;
  List: TStringlist; 
begin
  //Anlegen der Instanz... 
  List := TStringlist.Create;
  try
    GetNames(List);
    for i := 0 to List.Count - 1 do
      Stringverarbeitung(List[i]);
  finally
    //... und Freigabe
    List.Free;
  end; 
end;

Eine Benutzung der Methode wie im ersten Negativbeispiel ist somit ausgeschlossen (zumindest wüsste ich spontan nicht, wie man das hinbekommen sollte).

Ausnahmen

Lazy Initialization

Es gibt ja auch Programmierer, die mit Speicher geizen. Bei Verwendung folgender Klasse

type
  TMyClass = class
  private
    FNames: TStrings;
    function GetNames: TStrings;
    procedure SetNames(const NewNames: TStrings);
  public
    destructor Destroy; override;
    property Names: TStrings read GetNames write SetNames;
  end;

... 

implementation

destructor TMyClass.Destroy; 
begin
  FNames.Free;
  inherited; 
end; 

function TMyClass.GetNames: TStrings; 
begin
  if not Assigned(FNames) then 
    FNames := TStringlist.Create;
  Result := FNames; 
end; 

procedure TMyClass.SetNames(const NewNames: TStrings); 
begin
  Names.Assign(NewNames); 
end;

wird die TStrings-Instanz FNames nur dann angelegt, wenn irgendwo auf die Property Names zugegriffen wird. Davon mag man halten, was man will, möglich ist es zumindest. Beachten Sie bitte: die Instanz wird entweder gar nicht oder höchstens einmalig erzeugt und dann FNames zugewiesen. Dies geschieht lediglich intern, so dass die Gefahr, Speicherlecks zu produzieren, hier nicht gegeben ist.

Funktion erzeugt die Instanz überhaupt nicht

Wir dürfen bei alledem nicht außer Acht lassen, dass die oben gezeigten Funktionen die zurückgegebene Instanz selbst anlegen. Das muss aber ja nicht zwingend der Fall sein. Angenommen, Sie schreiben ein mehrsprachiges Programm, das Schlüsselwörter in je einer Stringliste pro Sprache enthält. Je nachdem, welche Sprache gerade eingestellt ist, soll die Property Keywords die passende Liste bereitstellen.

type
  TTranslate = class
  private
    FLanguage: TLanguage;
    FEnglish: TStrings;
    FFrench: TStrings;
    FGerman: TStrings;
    FRussian: TStrings;
    function GetKeywords: TStrings;
  public
    property Keywords: TStrings read GetKeywords;
    property Language: TLanguage read FLanguage write FLanguage;
  end;

... 

implementation 

function TTranslate.GetKeywords: TStrings; 
begin
  case FLanguage of
    lgEnglish:
      Result := FEnglish;
    lgFrench:
      Result := FFrench;
    lgGerman:
      Result := FGerman;
    lgRussian:
      Result := FRussian;
    else
      raise Exception.Create('Invalid Language');
  end; 
end;

Hier geht das völlig in Ordnung, da die Instanzen ja im Konstruktor (oder spätestens im Getter, s.o.) angelegt werden und nicht in der Funktion selbst. Instanzierung und Freigabe erfolgen somit auf derselben Ebene.