web-dev-qa-db-fra.com

Pourquoi ne devrais-je pas utiliser "if Assigned ()" avant d'accéder aux objets?

Cette question est la continuation d'un commentaire particulier de personnes sur stackoverflow que j'ai vu plusieurs fois maintenant. Avec le développeur qui m'a appris Delphi, afin de protéger les choses, j'ai toujours mis une coche if assigned() avant de libérer des objets et avant de faire d'autres choses. Cependant, on me dit maintenant que je ne devrais pas ajouter cette vérification. J'aimerais savoir s'il y a une différence dans la façon dont l'application se compile/s'exécute si je fais cela, ou si cela n'affectera pas du tout le résultat ...

if assigned(SomeObject) then SomeObject.Free;

Disons que j'ai un formulaire, et je crée un objet bitmap en arrière-plan lors de la création du formulaire, et le libère lorsque j'en ai fini. Maintenant, je suppose que mon problème est que je me suis trop habitué à mettre cette vérification sur une grande partie de mon code lorsque j'essaie d'accéder à des objets qui pourraient potentiellement avoir été libérés à un moment donné. Je l'utilise même quand ce n'est pas nécessaire. J'aime être minutieux ...

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.

Supposons maintenant que j'introduise un nouvel objet de liste personnalisé appelé TMyList of TMyListItem. Pour chaque élément de cette liste, je dois bien sûr créer/libérer chaque objet élément. Il existe plusieurs façons de créer un élément, ainsi que plusieurs façons de détruire un élément (Ajouter/Supprimer étant le plus courant). Je suis sûr que c'est une très bonne pratique de mettre cette protection ici ...

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;

Dans de nombreux scénarios, j'espère au moins que l'objet est toujours créé avant d'essayer de le libérer. Mais vous ne savez jamais quels glissements pourraient se produire à l'avenir lorsqu'un objet se libère avant qu'il ne soit censé le faire. J'ai toujours utilisé ce chèque, mais maintenant on me dit que je ne devrais pas, et je ne comprends toujours pas pourquoi.


MODIFIER

Voici un exemple pour essayer de vous expliquer pourquoi j'ai l'habitude de le faire:

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

Mon point est que if SomeCreatedObject <> nil N'est pas identique à if Assigned(SomeCreatedObject) car après avoir libéré SomeCreatedObject, il n'évalue pas à nil. Les deux vérifications devraient donc être nécessaires.

56
Jerry Dodge

Il s'agit d'une question très large sous de nombreux angles différents.

La signification de la fonction Assigned

Une grande partie du code de votre question trahit une compréhension incorrecte de la fonction Assigned. documentation indique ceci:

Teste un pointeur nul (non assigné) ou une variable procédurale.

Utilisez Attribué pour déterminer si le pointeur ou la procédure référencée par P est nul . P doit être une référence variable d'un pointeur ou d'un type procédural.

Attribué (P) correspond au test P <> nul pour un variable pointeur, et @ P <> nil pour une variable procédurale.

Attribué renvoie Faux si P est nil , Vrai sinon.

Conseil : lorsque vous testez des événements d'objet et des procédures d'affectation, vous ne pouvez pas tester nil , et utiliser Assigned est la bonne façon.

....

Remarque : Attribué ne peut pas détecter un pointeur pendant - c'est-à-dire un ce n'est pas nul , mais cela ne pointe plus vers des données valides.

La signification de Assigned diffère pour le pointeur et les variables procédurales. Dans le reste de cette réponse, nous ne considérerons que les variables de pointeur, car c'est le contexte de la question. Notez qu'une référence d'objet est implémentée en tant que variable de pointeur.

Les points clés à retenir de la documentation sont que, pour les variables de pointeur:

  1. Assigned équivaut à tester <> nil.
  2. Assigned ne peut pas détecter si le pointeur ou la référence d'objet est valide ou non.

Dans le contexte de cette question, cela signifie que

if obj<>nil

et

if Assigned(obj)

sont complètement interchangeables.

Test de Assigned avant d'appeler Free

L'implémentation de TObject.Free est très spéciale.

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

Cela vous permet d'appeler Free sur une référence d'objet qui est nil et cela n'a aucun effet. Pour ce que ça vaut, je ne connais aucun autre endroit dans la RTL/VCL où une telle astuce est utilisée.

La raison pour laquelle vous souhaitez autoriser Free à être appelée sur une référence d'objet nil découle de la façon dont les constructeurs et les destructeurs fonctionnent dans Delphi.

Lorsqu'une exception est levée dans un constructeur, le destructeur est appelé. Cela est fait afin de désallouer toutes les ressources qui ont été allouées dans cette partie du constructeur qui a réussi. Si Free n'était pas implémenté tel quel, alors les destructeurs devraient ressembler à ceci:

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

La pièce suivante du puzzle est que les constructeurs Delphi initialisent la mémoire d'instance à zéro . Cela signifie que tous les champs de référence d'objet non attribués sont nil.

Mettez tout cela ensemble et le code destructeur devient maintenant

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

Vous devez choisir cette dernière option car elle est beaucoup plus lisible.

Il existe un scénario dans lequel vous devez tester si la référence est affectée dans un destructeur. Si vous devez appeler une méthode sur l'objet avant de le détruire, vous devez clairement vous prémunir contre la possibilité qu'il soit nil. Donc, ce code courrait le risque d'un AV s'il apparaissait dans un destructeur:

FSettings.Save;
FSettings.Free;

Au lieu de cela, vous écrivez

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

Test de Assigned en dehors d'un destructeur

Vous parlez également d'écrire du code défensif en dehors d'un destructeur. Par exemple:

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;

Dans cette situation, il n'est à nouveau pas nécessaire de tester Assigned dans TMyObject.Update. La raison étant que vous ne pouvez tout simplement pas appeler TMyObject.Update À moins que le constructeur de TMyObject ait réussi. Et si le constructeur de TMyObject a réussi, vous savez avec certitude que FSettings a été attribué. Encore une fois, vous rendez votre code beaucoup moins lisible et plus difficile à maintenir en faisant des appels parasites à Assigned.

Il y a un scénario où vous devez écrire if Assigned Et c'est là que l'existence de l'objet en question est facultative. Par exemple

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;

Dans ce scénario, la classe prend en charge deux modes de fonctionnement, avec et sans journalisation. La décision est prise au moment de la construction et toute méthode faisant référence à l'objet de journalisation doit tester son existence.

Cette forme de code non rare rend d'autant plus important que vous n'utilisez pas d'appels parasites à Assigned pour les objets non facultatifs. Lorsque vous voyez if Assigned(FLogger) dans le code, cela devrait vous indiquer clairement que la classe peut fonctionner normalement avec FLogger inexistant. Si vous vaporisez des appels gratuits vers Assigned autour de votre code, vous perdez la possibilité de dire d'un coup d'œil si un objet doit toujours exister.

127
David Heffernan

Free a une logique particulière: il vérifie si Self est nil, et si c'est le cas, il revient sans rien faire - vous pouvez donc appeler en toute sécurité X.Free même si X est nil. Ceci est important lorsque vous écrivez des destructeurs - David a plus de détails dans sa réponse .

Vous pouvez consulter le code source de Free pour voir comment cela fonctionne. Je n'ai pas la source Delphi à portée de main, mais c'est quelque chose comme ça:

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

Ou, si vous préférez, vous pouvez le considérer comme le code équivalent en utilisant Assigned :

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

Vous pouvez écrire vos propres méthodes qui vérifient if Self <> nil, tant qu'elles sont méthodes d'instance statiques (c'est-à-dire pas virtual ou dynamic) (merci à David Heffernan pour le lien de documentation). Mais dans la bibliothèque Delphi, Free est la seule méthode que je connaisse qui utilise cette astuce.

Vous n'avez donc pas besoin de vérifier si la variable est Assigned avant d'appeler Free; il le fait déjà pour vous. C'est pourquoi il est recommandé d'appeler Free plutôt que d'appeler Destroy directement: si vous appelez Destroy sur une référence nil, vous obtiendrez une violation d'accès.

21
Joe White

Pourquoi vous ne devriez pas appeler

if Assigned(SomeObject) then 
  SomeObject.Free;

Tout simplement parce que vous exécuteriez quelque chose comme ça

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

Si vous appelez simplement SomeObject.Free; alors c'est juste

  if Assigned(SomeObject) then 
    SomeObject.Destroy;

Pour votre mise à jour, si vous avez peur de la référence d'instance d'objet, utilisez FreeAndNil. Il détruira et déférera votre objet

FreeAndNil(SomeObject);

C'est comme si vous appeliez

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