it-swarm-pt.tech

Por que não devo usar "if Assigned ()" antes de acessar objetos?

Esta pergunta é a continuação de um comentário específico de pessoas no stackoverflow que eu já vi algumas vezes diferentes agora. Eu, juntamente com o desenvolvedor que me ensinou o Delphi, para manter as coisas seguras, sempre marquei if assigned() antes de liberar objetos e antes de fazer outras coisas diversas. No entanto, agora me disseram que eu não deveria adicionar esta verificação. Gostaria de saber se há alguma diferença em como o aplicativo compila/executa, se eu fizer isso, ou se não afetará o resultado ...

if assigned(SomeObject) then SomeObject.Free;

Digamos que eu tenho um formulário e estou criando um objeto de bitmap em segundo plano após a criação do formulário e liberando-o quando terminar. Agora, acho que meu problema é que eu me acostumei a colocar essa verificação em grande parte do meu código ao tentar acessar objetos que poderiam ter sido liberados em algum momento. Eu tenho usado mesmo quando não é necessário. Eu gosto de ser meticuloso ...

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.

Agora, digamos que estou apresentando um novo objeto de lista personalizado chamado TMyList de TMyListItem. Para cada item desta lista, é claro que tenho que criar/liberar cada objeto de item. Existem algumas maneiras diferentes de criar um item, bem como algumas maneiras diferentes de destruir um item (Adicionar/Excluir é o mais comum). Tenho certeza de que é uma prática muito boa colocar essa proteção aqui ...

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;

Em muitos cenários, pelo menos eu espero que o objeto ainda seja criado antes de tentar liberá-lo. Mas você nunca sabe quais deslizamentos podem acontecer no futuro em que um objeto é libertado antes que ele deveria. Eu sempre usei essa verificação, mas agora me disseram que não deveria e ainda não entendo o porquê.


EDITAR

Aqui está um exemplo para tentar explicar por que eu tenho o hábito de fazer isso:

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

O que quero dizer é que if SomeCreatedObject <> nil Não é o mesmo que if Assigned(SomeCreatedObject) porque, após liberar SomeCreatedObject, ele não avalia como nil. Portanto, as duas verificações devem ser necessárias.

56
Jerry Dodge

Esta é uma pergunta muito ampla, com muitos ângulos diferentes.

O significado da função Assigned

Grande parte do código em sua pergunta revela um entendimento incorreto da função Assigned. A documentação afirma isso:

Testes para um ponteiro nulo (não atribuído) ou variável processual.

Use Atribuído para determinar se o ponteiro ou o procedimento referenciado por P é nulo P deve ser uma referência variável de um ponteiro ou tipo de procedimento.

Atribuído (P) corresponde ao teste P <> nulo para um variável de ponteiro e @ P <> nil para uma variável de procedimento.

Atribuído retorna Falso se P for nil , Verdadeiro caso contrário.

Dica : ao testar eventos e procedimentos de objetos para atribuição, você não pode testar nulo e usar Atribuído é o caminho certo.

....

Nota : Atribuído não pode detectar um ponteiro pendente - ou seja, um isso não é nulo , mas que não aponta mais para dados válidos.

O significado de Assigned difere para variáveis ​​de ponteiro e de procedimento. No restante desta resposta, consideraremos apenas as variáveis ​​indicadoras, pois esse é o contexto da pergunta. Observe que uma referência de objeto é implementada como uma variável de ponteiro.

Os pontos principais a serem retirados da documentação são os seguintes, para variáveis ​​de ponteiro:

  1. Assigned é equivalente a testar <> nil.
  2. Assigned não pode detectar se a referência de ponteiro ou objeto é válida ou não.

O que isso significa no contexto desta pergunta é que

if obj<>nil

e

if Assigned(obj)

são completamente intercambiáveis.

Testando Assigned antes de chamar Free

A implementação de TObject.Free é muito especial.

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

Isso permite que você chame Free em uma referência de objeto que seja nil e fazer isso não tem efeito. Pelo que vale, não conheço outro lugar no RTL/VCL em que esse truque é usado.

A razão pela qual você deseja permitir que Free seja chamado em uma referência de objeto nil deriva da maneira como construtores e destruidores operam no Delphi.

Quando uma exceção é gerada em um construtor, o destruidor é chamado. Isso é feito para desalocar quaisquer recursos que foram alocados na parte do construtor que obteve êxito. Se Free não foi implementado como está, os destruidores teriam que se parecer com isso:

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

A próxima parte do quebra-cabeças é que os construtores Delphi inicializam a memória da instância para zero . Isso significa que qualquer campo de referência de objeto não atribuído é nil.

Coloque tudo isso junto e o código destruidor agora se torna

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

Você deve escolher a última opção, porque é muito mais legível.

Há um cenário em que você precisa testar se a referência está atribuída em um destruidor. Se você precisar chamar qualquer método no objeto antes de destruí-lo, é claro que deve se proteger contra a possibilidade de ele ser nil. Portanto, esse código correria o risco de um AV se aparecesse em um destruidor:

FSettings.Save;
FSettings.Free;

Em vez disso, você escreve

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

Testando Assigned fora de um destruidor

Você também fala sobre escrever código defensivo fora de um destruidor. Por exemplo:

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;

Nessa situação, novamente não há necessidade de testar Assigned em TMyObject.Update. O motivo é que você simplesmente não pode chamar TMyObject.Update, A menos que o construtor de TMyObject tenha sido bem-sucedido. E se o construtor de TMyObject tiver êxito, você terá certeza de que FSettings foi atribuído. Então, novamente, você torna seu código muito menos legível e mais difícil de manter, fazendo chamadas espúrias para Assigned.

Há um cenário em que você precisa escrever if Assigned E é aí que a existência do objeto em questão é opcional. Por exemplo

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;

Nesse cenário, a classe suporta dois modos de operação, com e sem registro. A decisão é tomada no momento da construção e todos os métodos que se referem ao objeto de log devem testar sua existência.

Essa forma incomum de código torna ainda mais importante que você não use chamadas espúrias para Assigned para objetos não opcionais. Quando você vê if Assigned(FLogger) no código, isso deve ser uma indicação clara de que a classe pode operar normalmente com FLogger inexistente. Se você pulverizar chamadas gratuitas para Assigned em torno do seu código, você perde a capacidade de dizer rapidamente se um objeto deve ou não sempre existir.

127
David Heffernan

Free possui uma lógica especial: verifica se Self é nil e, nesse caso, retorna sem fazer nada - para que você possa chamar com segurança X.Free mesmo se X for nil. Isso é importante quando você está escrevendo destruidores - David tem mais detalhes em sua resposta .

Você pode procurar no código fonte Free para ver como ele funciona. Eu não tenho a fonte Delphi à mão, mas é algo como isto:

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

Ou, se preferir, você pode pensar nele como o código equivalente usando Assigned :

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

Você pode escrever seus próprios métodos que verificam if Self <> nil, desde que sejam métodos de instância estáticos (isto é, não virtual ou dynamic)) (obrigado a David Heffernan pelo link da documentação). Mas na biblioteca Delphi, Free é o único método que conheço que usa esse truque.

Portanto, você não precisa verificar se a variável é Assigned antes de chamar Free; já faz isso por você. É por isso que a recomendação é chamar Free em vez de chamar Destroy diretamente: se você chamar Destroy em uma referência nil, poderá obter uma violação de acesso.

21
Joe White

Por que você não deveria ligar

if Assigned(SomeObject) then 
  SomeObject.Free;

Simplesmente porque você executaria algo assim

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

Se você chamar apenas SomeObject.Free; então é só

  if Assigned(SomeObject) then 
    SomeObject.Destroy;

Para sua atualização, se você tiver medo da referência da instância do objeto, use FreeAndNil. Destruirá e desreferenciará seu objeto

FreeAndNil(SomeObject);

É semelhante como se você ligasse

SomeObject.Free;
SomeObject := nil;
17
Martin Reiner