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

Этот вопрос является продолжением конкретного комментария от людей о 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. Поэтому должны быть необходимы обе проверки.

Ответ 1

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

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

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

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

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

Assigned(P) corresponds to the test P <> nil for a pointer variable, и @P <> nil for a procedural variable.

Assigned returns False if P is nil, True otherwise.

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

....

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

Значение 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 вокруг вашего кода, то вы теряете возможность сразу определить, должен ли объект существовать всегда.

Ответ 2

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, вы получили бы нарушение прав доступа.

Ответ 3

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

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;

Ответ 4

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

if assigned(object.owner) then object.free 

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

if assigned(FBitmap.owner) then FBitmap.free