Почему я не должен использовать «if Assigned ()» перед доступом к объектам?

53

Этот вопрос является продолжением конкретного комментария от людей о stackoverflow, который я видел несколько раз. Я, вместе с разработчиком, который научил меня Delphi, чтобы все было в безопасности, всегда ставил чек if assigned() перед освобождением объектов и перед другими делами. Тем не менее, теперь мне говорят, что я не должен добавлять эту проверку. Я хотел бы знать, есть ли какая-либо разница в том, как приложение компилируется/запускается, если я это сделаю, или если оно вообще не повлияет на результат...

if assigned(SomeObject) then SomeObject.Free;

Скажем, у меня есть форма, и я создаю объект растрового изображения в фоновом режиме при создании формы и освобождая его, когда я закончил с ним. Теперь я думаю, что моя проблема заключается в том, что я слишком привык ставить эту проверку на свой код, когда пытаюсь получить доступ к объектам, которые потенциально могли быть свободными в какой-то момент. Я использовал его, даже если это не нужно. Мне нравится быть основательным...

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FBitmap: TBitmap;
  public
    function LoadBitmap(const Filename: String): Bool;
    property Bitmap: TBitmap read FBitmap;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  FBitmap:= TBitmap.Create;
  LoadBitmap('C:\Some Sample Bitmap.bmp');
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if assigned(FBitmap) then begin //<-----
    //Do some routine to close file
    FBitmap.Free;
  end;
end;

function TForm1.LoadBitmap(const Filename: String): Bool;
var
  EM: String;
  function CheckFile: Bool;
  begin
    Result:= False;
    //Check validity of file, return True if valid bitmap, etc.
  end;
begin
  Result:= False;
  EM:= '';
  if assigned(FBitmap) then begin //<-----
    if FileExists(Filename) then begin
      if CheckFile then begin
        try
          FBitmap.LoadFromFile(Filename);
        except
          on e: exception do begin
            EM:= EM + 'Failure loading bitmap: ' + e.Message + #10;
          end;
        end;
      end else begin
        EM:= EM + 'Specified file is not a valid bitmap.' + #10;
      end;
    end else begin
      EM:= EM + 'Specified filename does not exist.' + #10;
    end;
  end else begin
    EM:= EM + 'Bitmap object is not assigned.' + #10;
  end;
  if EM <> '' then begin
    raise Exception.Create('Failed to load bitmap: ' + #10 + EM);
  end;
end;

end.

Теперь скажем, что я представляю новый пользовательский объект списка TMyList из TMyListItem. Для каждого элемента в этом списке, конечно, мне нужно создать/бесплатно каждый объект объекта. Существует несколько различных способов создания элемента, а также несколько различных способов уничтожения элемента (добавление/удаление является наиболее распространенным). Я уверен, что это очень хорошая практика, чтобы поставить эту защиту здесь...

procedure TMyList.Delete(const Index: Integer);
var
  I: TMyListItem;
begin
  if (Index >= 0) and (Index < FItems.Count) then begin
    I:= TMyListItem(FItems.Objects[Index]);
    if assigned(I) then begin //<-----
      if I <> nil then begin
        I.DoSomethingBeforeFreeing('Some Param');
        I.Free;
      end;
    end;
    FItems.Delete(Index);
  end else begin
    raise Exception.Create('My object index out of bounds ('+IntToStr(Index)+')');
  end;
end;

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


ИЗМЕНИТЬ

Вот пример, чтобы попытаться объяснить вам, почему у меня есть привычка делать это:

procedure TForm1.FormDestroy(Sender: TObject);
begin
  SomeCreatedObject.Free;
  if SomeCreatedObject = nil then
    ShowMessage('Object is nil')
  else
    ShowMessage('Object is not nil');
end;

Моя точка зрения заключается в том, что if SomeCreatedObject <> nil не совпадает с if Assigned(SomeCreatedObject), потому что после освобождения SomeCreatedObject он не оценивает значение nil. Поэтому должны быть необходимы обе проверки.

  • 0
    Чем assigned(I) отличается от I <> nil ? (Обратите внимание, что я вообще не использую Delphi: p ~)
  • 0
    Назначено, является ли экземпляр переменной созданным, тогда как nil может быть не назначен и все еще быть читаемым.
Показать ещё 21 комментарий
Теги:

4 ответа

122
Лучший ответ

Это очень широкий вопрос с разных точек зрения.

Смысл Assigned функции

Большая часть кода в вашем вопросе выдает неверное понимание Assigned функции. Документация утверждает это:

Проверяет нулевой (неназначенный) указатель или процедурную переменную.

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

Назначенный (P) соответствует тесту P <> nil для переменной-указателя и @P <> nil для процедурной переменной.

Assigned возвращает False, если P равно нулю, в противном случае - True.

Совет: при тестировании событий объекта и процедур для назначения вы не можете проверить на ноль, и использование Assigned является правильным способом.

....

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

Значение Assigned отличается для указателя и процедурных переменных. В оставшейся части этого ответа мы рассмотрим только переменные-указатели, так как это контекст вопроса. Обратите внимание, что ссылка на объект реализована как переменная-указатель.

Ключевые моменты, которые следует взять из документации, для переменных-указателей:

  1. Assigned эквивалентно тестированию <> nil.
  2. Assigned не может определить, является ли указатель или ссылка на объект действительным или нет.

Что это означает в контексте этого вопроса?

if obj<>nil

а также

if Assigned(obj)

полностью взаимозаменяемы.

Тестирование Assigned до вызова Free

Реализация TObject.Free очень особенная.

procedure TObject.Free;
begin
  if Self <> nil then
    Destroy;
end;

Это позволяет вам вызывать Free для ссылки на объект, которая равна nil и это не имеет никакого эффекта. Что бы это ни стоило, я не знаю другого места в RTL/VCL, где бы использовался такой прием.

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

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

if obj1 <> nil then
  obj1.Free;
if obj2 <> nil then
  obj2.Free;
if obj3 <> nil then
  obj3.Free;
....

Следующая часть головоломки состоит в том, что конструкторы Delphi инициализируют память экземпляра нулями. Это означает, что любые неназначенные ссылочные поля объекта равны nil.

Соберите все это вместе, и код деструктора станет

obj1.Free;
obj2.Free;
obj3.Free;
....

Вы должны выбрать последний вариант, потому что он гораздо более читабелен.

Есть один сценарий, в котором вам нужно проверить, назначена ли ссылка в деструкторе. Если вам нужно вызвать какой-либо метод объекта перед его уничтожением, тогда вам следует остерегаться того, что он может быть nil. Таким образом, этот код будет рисковать AV, если он появится в деструкторе:

FSettings.Save;
FSettings.Free;

Вместо этого ты пишешь

if Assigned(FSettings) then
begin
  FSettings.Save;
  FSettings.Free;
end;

Тестирование Assigned вне деструктора

Вы также говорите о написании защитного кода вне деструктора. Например:

constructor TMyObject.Create;
begin
  inherited;
  FSettings := TSettings.Create;
end;

destructor TMyObject.Destroy;
begin
  FSettings.Free;
  inherited;
end;

procedure TMyObject.Update;
begin
  if Assigned(FSettings) then
    FSettings.Update;
end;

В этой ситуации снова нет необходимости тестировать Assigned в TMyObject.Update. Причина в том, что вы просто не можете вызвать TMyObject.Update если конструктор TMyObject успешно. И если конструктор TMyObject успешно, вы точно знаете, что FSettings был назначен. Итак, снова вы делаете свой код гораздо менее читабельным и трудным для поддержки, вводя ложные вызовы в Assigned.

Существует сценарий, в котором вам нужно написать, if Assigned и именно здесь существование рассматриваемого объекта не является обязательным. Например

constructor TMyObject.Create(UseLogging: Boolean);
begin
  inherited Create;
  if UseLogging then
    FLogger := TLogger.Create;
end;

destructor TMyObject.Destroy;
begin
  FLogger.Free;
  inherited;
end;

procedure TMyObject.FlushLog;
begin
  if Assigned(FLogger) then
    FLogger.Flush;
end;

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

Эта не редкая форма кода делает еще более важным, чтобы вы не использовали ложные вызовы Assigned для не необязательных объектов. Когда вы видите if Assigned(FLogger) в коде, это должно быть четким указанием на то, что класс может нормально работать с FLogger не существует. Если вы распыляете безвозмездные вызовы в Assigned вокруг вашего кода, вы теряете возможность сразу определить, должен ли объект существовать всегда.

  • 28
    +10 Но допускается только +1.
  • 3
    @David, в TMyObject.Destroy вы вызываете FLogger.Free не проверяя, назначен ли он. Это потому, что TMyObject.Create всегда инициализирует его на ноль, когда UseLogging имеет значение False? при объявлении локальной процедуры TObject в процедуре мы не можем просто вызвать object.Free без предварительной инициализации. или я не прав?
Показать ещё 18 комментариев
20

Free имеет некоторую специальную логику: он проверяет, есть ли Self nil, и если да, то он возвращается без каких-либо действий, поэтому вы можете безопасно вызвать X.Free, даже если X есть nil. Это важно, когда вы пишете деструкторы - у Дэвида более подробная информация в его ответе.

Вы можете посмотреть исходный код Free, чтобы узнать, как он работает. У меня нет источника Delphi, но это примерно так:

procedure TObject.Free;
begin
  if Self <> nil then
    Destroy;
end;

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

procedure TObject.Free;
begin
  if Assigned(Self) then
    Destroy;
end;

Вы можете написать свои собственные методы, которые проверяют на if Self <> nil, если они static (т.е. не virtual или dynamic) методы экземпляра (спасибо Дэвиду Хеффернану за ссылку на документацию). Но в библиотеке Delphi Free - единственный метод, который я знаю, который использует этот трюк.

Поэтому вам не нужно проверять, есть ли переменная Assigned перед вызовом Free; это уже делает это для вас. На самом деле, почему рекомендация заключается в вызове Free, а не в вызове Destroy напрямую: если вы вызвали Destroy на ссылку nil, вы получили бы нарушение прав доступа.

  • 0
    Ну, вопрос был не о нуле, а о assigned() который работает немного по-другому
  • 3
    Если вы не работаете с типом делегата ( procedure of object или function of object ), Assigned - это то же самое, что проверка <> nil .
Показать ещё 22 комментария
15

Почему вы не должны звонить

if Assigned(SomeObject) then 
  SomeObject.Free;

Просто потому, что вы выполнили бы что-то вроде этого

if Assigned(SomeObject) then 
  if Assigned(SomeObject) then 
    SomeObject.Destroy;

Если вы вызываете только SomeObject.Free;, тогда он просто

  if Assigned(SomeObject) then 
    SomeObject.Destroy;

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

FreeAndNil(SomeObject);

Это похоже на вызов

SomeObject.Free;
SomeObject := nil;
  • 2
    +10 за это !! Все обсуждение этой темы сводится к следующему: SomeObject.Free уничтожает экземпляр, вызывая цепочку деструкторов и освобождая выделенную память. Это не меняет значение SomeObject . Поскольку это указатель, то есть адрес памяти, он все равно будет нести этот же адрес после SomeObject.Free Кроме того, AFAIK, освобожденная память не заполняется нулевыми байтами, как TObject.InitInstance делает TObject.InitInstance при создании. Поэтому, в идеале, используйте FreeAndNil( SomeObject ) иначе нет возможности отличить живые экземпляры от мертвых.
-2

Я не совсем уверен в этом, но кажется:

if assigned(object.owner) then object.free 

работает отлично. В этом примере это будет

if assigned(FBitmap.owner) then FBitmap.free
  • 0
    нет, это все еще не работает. Я имею в виду, иногда это работает, иногда нет ... как указано выше.
  • 0
    но дополнительно тестирование "object <> nil" - также не решило проблему ... Я предпочитаю использовать другую переменную fex. msgstr "ifcreated_object: boolean". Ну, это работает отлично.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню