Избегайте вложенных попыток... наконец-то блоки в Delphi

У меня была эта идея сегодня утром, чтобы избежать вложенных блоков try, как показано ниже.

procedure DoSomething;
var
  T1, T2, T3 : TTestObject;
begin
  T1 := TTestObject.Create('One');
  try
    T2 := TTestObject.Create('Two');
    try
      T3 := TTestObject.Create('Three');
      try
        //A bunch of code;
      finally
        T3.Free;
      end;
    finally
      T2.Free;
    end;
  finally
    T1.Free;
  end;
end;

Воспользовавшись автоматическим подсчетом ссылок на интерфейсы, я придумал

Type  
  IDoFinally = interface
    procedure DoFree(O : TObject);
  end;

  TDoFinally = class(TInterfacedObject, IDoFinally)
  private
    FreeObjectList : TObjectList;
  public
    procedure DoFree(O : TObject);
    constructor Create;
    destructor Destroy; override;
  end;

//...

procedure TDoFinally.DoFree(O : TObject);
begin
  FreeObjectList.Add(O);
end;

constructor TDoFinally.Create;
begin
  FreeObjectList := TObjectList.Create(True);
end;

destructor TDoFinally.Destroy;
begin
  FreeObjectList.Free;
  inherited;
end;

Итак, предыдущий блок кода становится

procedure DoSomething;
var
  T1, T2, T3 : TTestObject;
  DoFinally : IDoFinally;
begin
  DoFinally := TDoFinally.Create;
  T1 := TTestObject.Create('One');
  DoFinally.DoFree(T1);
  T2 := TTestObject.Create('Two');
  DoFinally.DoFree(T2);
  T3 := TTestObject.Create('Three');
  DoFinally.DoFree(T3);
  // A Bunch of code;
end;

Мой вопрос: это работает или я что-то пропустил?

Для меня это выглядит довольно круто и делает код немного легче читать с уменьшенным количеством вложенности. Он также может быть расширен для хранения списка анонимных методов для выполнения таких действий, как закрытие файлов, запросов и т.д.

Ответы

Ответ 1

Да, он работает.

Возможно, единственное, что отличается между вложенными блоками try-finally исходного кода и техникой использования объекта с подсчетом ссылок для управления временем жизни других объектов, - это то, что происходит, если есть проблема, разрушающая любой из объектов. Если существует исключение, когда какой-либо объект уничтожается, вложенные блоки try-finally гарантируют, что все оставшиеся объекты будут освобождены. TObjectList в вашем TDoFinally не делает этого; если какой-либо элемент в списке не может быть уничтожен, будут удалены любые последующие элементы в списке.

На практике это не проблема. Ни один деструктор не должен бросать исключение. Если это так, на самом деле не существует способа восстановить его, так что не имеет значения, из-за чего что-то утечки. Ваша программа должна прекратиться на мгновение в любом случае, поэтому наличие аккуратной процедуры очистки не имеет большого значения.

Кстати, JCL уже предлагает интерфейсы ISafeGuard и IMultiSafeGuard для управления временем жизни локальных объектов. Например, вы можете переписать свой код следующим образом:

uses JclSysUtils;

procedure DoSomething;
var
  T1, T2, T3: TTestObject;
  G: IMultiSafeGuard;
begin
  T1 := TTestObject(Guard(TTestObject.Create('One'), G));
  T2 := TTestObject(Guard(TTestObject.Create('Two'), G));
  T3 := TTestObject(Guard(TTestObject.Create('Three'), G));
  // A Bunch of code;
end;

Эта библиотека не адресует исключения в деструкторах.

Ответ 2

Я обычно делаю что-то вроде этого, так как обеспечивает баланс между читабельностью кода и сложностью:

procedure DoSomething;
var
  T1, T2, T3 : TTestObject;
begin
  T1 := nil;
  T2 := nil;
  T3 := nil;
  try
    T1 := TTestObject.Create('One');
    T2 := TTestObject.Create('Two');
    T3 := TTestObject.Create('Three');

    // A bunch of code

  finally
    T3.Free;
    T2.Free;
    T1.Free;
  end;
end;

Внимание:

  • Это не совсем эквивалентно исходному коду, потому что если T3.Free выдает исключение, T2 и T1 не освободятся и не вызовут утечку памяти, а то же самое для T2.Free в T1.

  • Однако, как говорит Роб Кеннеди в своем комментарии и более подробно объясняет его ответ, он эквивалентен вашему альтернативному коду, используя IDoFinally.

  • Итак, ваши два подхода не совсем эквивалентны.

Ответ 3

Умные указатели - это еще один способ добиться автоматического управления памятью.

На веб-сайте ADUG есть версия из статей Барри Келли о том, как внедрять интеллектуальные указатели строго ввели в Delphi с использованием дженериков, анонимных методов и интерфейсов:

Ваш код будет переписан следующим образом:

procedure DoSomething;
var
  T1, T2, T3 : ISmartPointer<TTestObject>;
begin
  T1 := TSmartPointer<TTestObject>.Create(TTestObject.Create('One'));
  T2 := TSmartPointer<TTestObject>.Create(TTestObject.Create('Two'));
  T3 := TSmartPointer<TTestObject>.Create(TTestObject.Create('Three'));

  // A bunch of code
end;

Ответ 4

У меня есть набор вспомогательных функций, чтобы сделать подход @JRL более удобоваримым.

procedure InitialiseNil(var Obj1); overload;
procedure InitialiseNil(var Obj1, Obj2); overload;
procedure InitialiseNil(var Obj1, Obj2, Obj3); overload;

procedure FreeAndNil(var Obj1); overload;
procedure FreeAndNil(var Obj1, Obj2); overload;
procedure FreeAndNil(var Obj1, Obj2, Obj3); overload;

На самом деле мой код имеет версии с еще большим количеством параметров. Для простоты обслуживания этот код автоматически генерируется из короткого сценария Python.

Эти методы реализуются очевидным образом, например

procedure FreeAndNil(var Obj1, Obj2);
var
  Temp1, Temp2: TObject;
begin
  Temp1 := TObject(Obj1);
  Temp2 := TObject(Obj2);
  Pointer(Obj1) := nil;
  Pointer(Obj2) := nil;
  Temp1.Free;
  Temp2.Free;
end;

Это позволяет нам переписать код в вопросе следующим образом:

InitialiseNil(T1, T2, T3);
try
  T1 := TTestObject.Create('One');
  T2 := TTestObject.Create('Two');
  T3 := TTestObject.Create('Three');
finally
  FreeAndNil(T3, T2, T1);
end;

И скрипт Python:

count = 8


def VarList(count, prefix):
    s = ""
    for i in range(count):
        if i != 0:
            s = s + ", "
        s = s + prefix + str(i + 1)
    return s


def InitialiseNilIntf(count):
    print("procedure InitialiseNil(var " + VarList(count, "Obj") + "); overload;")


def FreeAndNilIntf(count):
    print("procedure FreeAndNil(var " + VarList(count, "Obj") + "); overload;")


def InitialiseNilImpl(count):
    print("procedure InitialiseNil(var " + VarList(count, "Obj") + ");")
    print("begin")
    for i in range(count):
        print("  Pointer(Obj%s) := nil;" % str(i + 1))
    print("end;")
    print()


def FreeAndNilImpl(count):
    print("procedure FreeAndNil(var " + VarList(count, "Obj") + ");")
    print("var")
    print("  " + VarList(count, "Temp") + ": TObject;")
    print("begin")
    for i in range(count):
        print("  Temp%s := TObject(Obj%s);" % (str(i + 1), str(i + 1)))
    for i in range(count):
        print("  Pointer(Obj%s) := nil;" % str(i + 1))
    for i in range(count):
        print("  Temp%s.Free;" % str(i + 1))
    print("end;")
    print()


for i in range(count):
    InitialiseNilIntf(i + 1)
print()
for i in range(count):
    FreeAndNilIntf(i + 1)
print()
for i in range(count):
    InitialiseNilImpl(i + 1)
print()
for i in range(count):
    FreeAndNilImpl(i + 1)

Ответ 5

Альтернатива, которую я иногда использую:

procedure DoSomething;
var
  T1, T2, T3: TTestObject;
begin
  T1 := nil;
  T2 := nil;
  T3 := nil;
  try
    T1 := TTestObject.Create;
    T2 := TTestObject.Create;
    T3 := TTestObject.Create;
    // ...
  finally
    T1.Free;
    T2.Free;
    T3.Free;
  end;
end;

Ответ 6

Да, этот код работает, хотя я лично был бы склонен добавить inherited к вашему конструктору и деструктору.

Существует множество библиотек, в которых есть реализации, которые используют этот механизм. Последние компиляторы Delphi для мобильных платформ управляют временем жизни объектов, используя ARC, автоматический подсчет ссылок, который является одним и тем же методом, но испекли в обработке компилятора ссылок на объекты.

Ответ 7

Здесь немного отличается реализация одной и той же идеи:

unit ObjectGuard;

interface

type
  TObjectReference = ^TObject;

  { TObjectGuard }
  TObjectGuard = class(TInterfacedObject)
  private
    fUsed: integer;
    fObjectVariable: array [0..9] of TObjectReference;
  public
    constructor Create(var v0); overload;
    constructor Create(var v0, v1); overload;
// add overloaded constructors for 3,4,5... variables
    destructor Destroy; override;
  end;

implementation

constructor TObjectGuard.Create(var v0);
begin
  fObjectVariable[0] := @TObject(v0);
  Tobject(v0) := nil;
  fUsed := 1;
end;

constructor TObjectGuard.Create(var v0, v1);
begin
  fObjectVariable[0] := @TObject(v0);
  Tobject(v0) := nil;
  fObjectVariable[1] := @TObject(v1);
  Tobject(v1) := nil;
  fUsed := 2;
end;

destructor TObjectGuard.Destroy;
var
  i: integer;
begin
  for i := 0 to FUsed - 1 do
    if Assigned(fObjectVariable[i]^) then
    begin
      fObjectVariable[i]^.Free;
      fObjectVariable[i]^ := nil;
    end;
  inherited;
end;

end.

Преимуществом является простое использование, например:

procedure Test;
var
  Guard: IInterface
  vStringList: TStringList;
  vForm: TForm;
begin
  Guard := TObjectGuard.Create(vStringList, vForm);
  vStringList := TStringList.Create;
  vForm:= TForm.Create(nil);
  // code that does something
end;

Удобно, что вы можете создать Guard в начале метода и передать любое количество переменных в один вызов. Поэтому вам не нужно сначала создавать экземпляры объектов.

Также обратите внимание, что переменные автоматически инициализируются нулем в конструкторе.

Изменить: Кроме того, из-за того, что время жизни интерфейса равно времени выполнения метода, мы можем использовать его для профилирования, возможно, IFDEF-ed для упрощения управления.

Ответ 8

Я не вижу необходимости обертывать деструктор в интерфейсе. По умолчанию Delphi создает заставку try/finally в каждой процедуре/функции, которая использует интерфейсы, в которых счетчик ссылок интерфейсов уменьшается, тем самым вызывая деструктор, когда он достигает нуля.

У меня была быстрая проверка, но (по крайней мере, в Delphi 7) исключение в одном деструкторе остановит другие деструкторы, к сожалению. Один из способов остановить это - написать try/except в каждом деструкторе, но это снова больше кода в другом месте, чтобы просто сохранить код в первую очередь...

type
  IMyIntf=interface(IInterface)
    function GetName:string;
    procedure SetName(const Name:string);
    property Name:string read GetName write SetName;
  end;

  TMyObj=class(TInterfacedObject, IMyIntf)
  private
    FName:string;
    function GetName:string;
    procedure SetName(const Name:string);
  public
    constructor Create(const Name:string);
    destructor Destroy; override;
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  x,y:IMyIntf;
begin
  x:=TMyObj.Create('a');
  y:=TMyObj.Create('b');

  x.Name:='x';
  y.Name:='y';
end;

{ TMyObj }

constructor TMyObj.Create(const Name: string);
begin
  inherited Create;
  FName:=Name;
end;

destructor TMyObj.Destroy;
begin
  MessageBox(Application.Handle,PChar(FName),'test',MB_OK);
  //test: raise Exception.Create('Destructor '+FName);
  inherited;
end;

function TMyObj.GetName: string;
begin
  Result:=FName;
end;

procedure TMyObj.SetName(const Name: string);
begin
  FName:=Name;
end;

Ответ 9

Трудно понять, какой метод использовать на основе конечной цели, но в некоторых случаях я стараюсь реализовать подпрограммы или вообще разделить мой код на разные функции. Например...

FOne: TSomeObject;
FTwo: TSomeObject;
FThree: TSomeObject;

....

procedure DoSomething;
begin
  FOne:= TSomeObject.Create;
  try
    //a bunch of code which only needs FOne
    DoSomethingElse;
  finally
    FOne.Free;
  end;
end;

procedure DoSomethingElse;
begin
  FTwo:= TSomeObject.Create;
  try
    ShowMessage(DoYetAnother);
    //A bunch of code that requires FTwo
  finally
    FTwo.Free;
  end;
end;

function DoYetAnother: String;
begin
  FThree:= TSomeObject.Create;
  try
    //Do something with FOne, FTwo, and FThree
    Result:= FOne.Something + FTwo.Something + FThree.Something;
  finally
    FThree.Free;
  end;
end;

Опять же, вам трудно понять, как это будет работать без более реального сценария. Я все еще думаю о хорошем примере и с радостью отредактирую, когда подумаю. Общая идея, однако, заключается в разделении различных сегментов бизнес-правил на разные повторно используемые блоки кода.

В качестве альтернативы вместо объявления глобальных переменных вы также можете передавать параметры от одной процедуры к следующей.