Извлечь вложенные блоки try / finally

Как бы вы «извлекли» вложенные блоки try / finally из подпрограммы в повторно используемый объект? Скажи, что у меня есть

procedure DoSomething;
var
  Resource1: TSomeKindOfHandleOrReference1;
  Resource2: TSomeKindOfHandleOrReference2;
  Resource3: TSomeKindOfHandleOrReference3;
begin
  AcquireResource1;
  try
    AcquireResource2;
    try
      AcquireResource3;
      try
        // Use the resources
      finally
        ReleaseResource3;
      end;
    finally
      ReleaseResource2;
    end;
  finally
    ReleaseResource1;
  end;
end;

и хочу что-то вроде

TDoSomething = record // or class
strict private
  Resource1: TSomeKindOfHandleOrReference1;
  Resource2: TSomeKindOfHandleOrReference2;
  Resource3: TSomeKindOfHandleOrReference3;
public
  procedure Init; // or constructor
  procedure Done; // or destructor
  procedure UseResources;
end;

procedure DoSomething;
var
  Context: TDoSomething;
begin
  Context.Init;
  try
    Context.UseResources;
  finally
    Context.Done;
  end;
end;

Я хочу, чтобы у этого была такая же безопасность исключений, как у вложенного оригинала. Достаточно ли обнулить переменные ResourceN в TDoSomething.Init и выполнить несколько if Assigned(ResourceN) then проверок в TDoSomething.Done?


person Uli Gerhardt    schedule 07.04.2011    source источник
comment
@Мистер. Разочарование: если это облегчит вашу боль, представьте, что три вложенных блока извлекаются в три подпрограммы, которые затем вызываются вложенным образом. :-) Суть проблемы не меняет.   -  person Uli Gerhardt    schedule 07.04.2011
comment
Эй, куда делся этот комментарий? :-)   -  person Uli Gerhardt    schedule 07.04.2011
comment
Это так - я удалил комментарий, когда перебил "OMG!" момент довольно быстро, поскольку я не парень Delphi, и внезапно вспомнил намного хуже. ;) Но я бы спросил: почему вы не можете просто использовать один try / finally, определяя, какой ресурс не был получен, и избавляясь от тех, которые были? Я предполагаю, что именно к этому вы и направляетесь со своим подходом.   -  person Grant Thomas    schedule 07.04.2011
comment
да. Я просто спросил, потому что я не хочу случайно выбросить из окна защиту от исключений.   -  person Uli Gerhardt    schedule 07.04.2011


Ответы (3)


В классах есть три особенности, которые делают эту идиому безопасной и простой:

  1. Во время фазы выделения памяти конструктора (перед запуском реального тела конструктора) поля ссылки на класс инициализируются значением nil.
  2. Когда в конструкторе возникает исключение, деструктор вызывается автоматически.
  3. Всегда безопасно вызывать Free по нулевой ссылке, поэтому вам никогда не нужно сначала проверять Assigned.

Поскольку деструктор может полагаться на то, что все поля имеют известные значения, он может безопасно вызывать Free для всего, независимо от того, как далеко ушел конструктор до сбоя. Каждое поле либо будет содержать действительную ссылку на объект, либо будет нулевым, и в любом случае его можно безопасно освободить.

constructor TDoSomething.Create;
begin
  Resource1 := AcquireResource1;
  Resource2 := AcquireResource2;
  Resource3 := AcquireResource3;
end;

destructor TDoSomething.Destroy;
begin
  Resource1.Free;
  Resource2.Free;
  Resource3.Free;
end;

Используйте его так же, как и любой другой класс:

Context := TDoSomething.Create;
try
  Context.UseResources;
finally
  Context.Free;
end;
person Rob Kennedy    schedule 07.04.2011
comment
Я не могу воспользоваться пунктом 3, потому что ReleaseResource не обязательно является вызовом TObject.Free. Но я могу исправить это с помощью if Assigned, и пункт 2 в любом случае кажется важным моментом. Так что я останусь с идиоматическим подходом и пожну плоды. - person Uli Gerhardt; 07.04.2011
comment
Я обычно помещаю вызов contructor внутрь блока try..finally. Я просто понял (по вашему пункту 2), что в этом нет необходимости. Я не уверен, изменю ли я свою привычку, поскольку нахожу это более ясным. Что вы думаете? - person PA.; 07.04.2011
comment
Следуйте примеру TObject.Free и FreeMem и make ReleaseResource безопасным для вызова нулевого ресурса. В остальном это значительно упрощает жизнь. - person Rob Kennedy; 07.04.2011
comment
@PA это не только не обязательно, но и неправильно. Если конструктор выбрасывает, то переменная, которой вы назначаете результат, не инициализируется, поэтому вы вызываете Free для неинициализированного значения. - person Rob Kennedy; 07.04.2011

Да, вы можете использовать один блок try / finally / end для нескольких ресурсов с нулевой инициализацией.

Другое возможное решение можно найти в блоге Барри Келли

person kludg    schedule 07.04.2011
comment
Спасибо за идею. Мне в голову уже приходил какой-то защитный интерфейс, но он всегда кажется мне излишним. Кстати: я прокомментировал сообщение Барри. :-) - person Uli Gerhardt; 07.04.2011

В исходном коде Delphi используется шаблон с тестированием на Assigned in finally. Вы делаете то же самое, но я думаю, вам следует переместить Context.Init для захвата исключения из Context.Init.

procedure DoSomething;
var
  Context: TDoSomething;
begin
  try
    Context.Init;
    Context.UseResources;
  finally
    Context.Done;
  end;
end;

Редактировать 1 Вот как вы должны это сделать без Context.Init и Context.Done. Если вы поместите весь код AquireResource перед try, вы не освободите Resource1, если получите исключение в AcquireResource2

procedure DoSomething;
var
    Resource1: TSomeKindOfHandleOrReference1;
    Resource2: TSomeKindOfHandleOrReference2;
    Resource3: TSomeKindOfHandleOrReference3;
begin
    Resource1 := nil;
    Resource2 := nil;
    Resource3 := nil;
    try
        AcquireResource1;
        AcquireResource2;
        AcquireResource3;

        //Use the resources

    finally
        if assigned(Resource1) then ReleaseResource1;
        if assigned(Resource2) then ReleaseResource2;
        if assigned(Resource3) then ReleaseResource3;
    end;
end;
person Mikael Eriksson    schedule 07.04.2011
comment
Хм, размещение Init внутри блока try кажется неправильным. Вы предлагаете это, потому что я сделал TDoSomething типом записи. Если бы это был класс, вы бы написали try Context := TDoSomething.Create;? - person Uli Gerhardt; 07.04.2011
comment
Это кажется неправильным, потому что это неверно. Если инициализация вызывает исключение, вы не хотите ничего финализировать, потому что вы не знаете, что безопасно для завершения. - person Rob Kennedy; 07.04.2011
comment
@Mikael: Вероятно, это та же проблема, что и в stackoverflow.com/q/398137/35162. Я помню, что было много дискуссий по очень тонкостям. ;-) - person Uli Gerhardt; 07.04.2011
comment
@Ulrich - Да, принятый ответ такой же, как я предлагаю. Значит ли это, что ваш вопрос следует пометить как повторяющийся :)? - person Mikael Eriksson; 07.04.2011
comment
Нет, Микаэль, принятый ответ не совпадает с тем, что вы здесь показали. Если AcquireResource2 вызывает исключение, Resource2 и Resource3 еще не инициализированы, поэтому проверка их текущих значений с помощью Assigned является ошибкой. Вам нужно инициализировать вещи перед вводом блока try. - person Rob Kennedy; 07.04.2011
comment
@Rob - обновлю нужным кодом инициализации. Из вопроса я предположил, что это делается в Init. Достаточно ли обнулить переменные ResourceN в TDoSomething.Init - person Mikael Eriksson; 07.04.2011
comment
@Mikael: Должен признать, что ретроспективно мой вопрос выглядит как дубликат. - person Uli Gerhardt; 07.04.2011
comment
@Mikael: Я принял ответ Роба, потому что он выглядит более идиоматичным / Delphi'ish и, вероятно, это то, что разработчики языка хотят, чтобы я написал. Спасибо за ваш вклад! - person Uli Gerhardt; 07.04.2011