Избегайте вложенных попыток... наконец-то блоки в 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;
Опять же, вам трудно понять, как это будет работать без более реального сценария. Я все еще думаю о хорошем примере и с радостью отредактирую, когда подумаю. Общая идея, однако, заключается в разделении различных сегментов бизнес-правил на разные повторно используемые блоки кода.
В качестве альтернативы вместо объявления глобальных переменных вы также можете передавать параметры от одной процедуры к следующей.