Pourquoi ne devrais-je pas utiliser “if Assigned ()” avant d’accéder aux objects?

Cette question est une continuation d’un commentaire particulier de personnes sur stackoverflow que j’ai vu quelques fois différentes maintenant. Avec le développeur qui m’a appris Delphi, afin de garder les choses en sécurité, j’ai toujours placé une coche if assigned() avant de libérer des objects et avant de faire d’autres choses. Cependant, on me dit maintenant que je ne devrais pas append ce chèque. J’aimerais savoir s’il y a une différence dans la façon dont l’application comstack / s’exécute si je le fais, ou si cela n’affecte pas du tout le résultat …

 if assigned(SomeObject) then SomeObject.Free; 

Disons que j’ai un formulaire et que je crée un object 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 suis trop habitué à mettre cette vérification sur beaucoup de mes codes lorsque j’essaie d’accéder à des objects qui pourraient avoir été libérés à un moment donné. Je l’utilise même si 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: Ssortingng): 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. 

Maintenant, disons que je présente un nouvel object de liste personnalisé appelé TMyList de TMyListItem . Pour chaque élément de cette liste, je dois bien sûr créer / libérer chaque object. Il existe plusieurs manières différentes de créer un élément, ainsi que quelques moyens différents de le détruire (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 beaucoup de scénarios, au moins j’espère que l’object est toujours créé avant d’essayer de le libérer. Mais vous ne savez jamais ce qui pourrait arriver dans le futur où un object est libéré avant qu’il soit supposé. 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 le même que if Assigned(SomeCreatedObject) parce qu’après avoir libéré SomeCreatedObject , il n’est pas évalué à nil . Les deux vérifications doivent donc être nécessaires.

    C’est une question très large avec de nombreux angles différents.

    La signification de la fonction Assigned

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

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

    Utilisez Assigned pour déterminer si le pointeur ou la procédure référencé par P est nul. P doit être une référence de variable d’un pointeur ou d’un type procédural. Assigned (P) correspond au test P <> nil pour une variable de pointeur, et @P <> nil pour une variable procédurale.

    Assigned renvoie False si P est nul, True sinon.

    Remarque : Assigned ne peut pas détecter un pointeur en attente, c’est-à-dire qui n’est pas nul mais ne pointe plus vers des données valides. Par exemple, dans l’exemple de code pour Assigned, Assigned ne détecte pas que P n’est pas valide.

    Les points clés à prendre en compte sont les suivants:

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

    Qu’est-ce que cela signifie dans le contexte de cette question est que

     if obj<>nil 

    et

     if Assigned(obj) 

    sont complètement interchangeables.

    Tests 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’object qui est nil et cela n’a aucun effet. Pour ce que cela vaut, je ne connais aucun autre endroit dans la RTL / VCL où une telle astuce est utilisée.

    La raison pour laquelle vous voulez autoriser Free à être appelé sur une référence d’object nil provient de la manière dont fonctionnent les constructeurs et les destructeurs dans Delphi.

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

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

    Le morceau suivant 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’object non affecté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 où vous devez tester si la référence est affectée dans un destructeur. Si vous avez besoin d’appeler n’importe quelle méthode sur l’object avant de le détruire, vous devez clairement vous protéger 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; 

    Tester Assigned extérieur d’un destructeur

    Vous parlez également d’écrire du code défensif à l’extérieur 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 plus nécessaire de tester Assigned dans TMyObject.Update . La raison en est que vous ne pouvez simplement pas appeler TMyObject.Update moins que le constructeur de TMyObject réussi. Et si le constructeur de TMyObject réussi, alors vous savez avec certitude que les FSettings été assignés. Donc, encore une fois, vous rendez votre code moins lisible et plus difficile à gérer en envoyant des appels erronés à Assigned .

    Il y a un scénario où vous devez écrire if Assigned et c’est là que l’existence de l’object 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’object de journalisation doit vérifier son existence.

    Cette forme de code peu commune rend encore plus important que vous n’utilisiez pas d’appels intempestifs à Assigned pour des objects non optionnels. Lorsque vous voyez if Assigned(FLogger) dans le code, cela devrait vous indiquer clairement que la classe ne peut pas fonctionner normalement avec FLogger . Si vous pulvérisez des appels gratuits vers Assigned autour de votre code, vous perdez la possibilité de dire d’un coup d’œil si un object doit toujours exister ou non.

    Free a une logique particulière: il vérifie si Self est nil , et si oui, il retourne sans rien faire – vous pouvez donc appeler X.Free toute sécurité, 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 regarder le code source de Free pour voir comment cela fonctionne. Je n’ai pas la source Delphi à scope 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 pourriez 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 , à condition qu’elles soient des méthodes d’instance statiques (c’est-à-dire pas virtual ou dynamic ) (merci à David Heffernan pour le lien 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 ; c’est déjà fait pour vous. C’est en fait la raison pour laquelle il est recommandé d’appeler Free plutôt que d’appeler directement Destroy : si vous appelez Destroy sur une référence nil , vous obtiendrez une violation d’access.

    Pourquoi tu ne devrais pas appeler

     if Assigned(SomeObject) then SomeObject.Free; 

    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’object, utilisez FreeAndNil. Cela détruira et déréférencera votre object

     FreeAndNil(SomeObject); 

    C’est comme si vous appelez

     SomeObject.Free; SomeObject := nil; 

    Je n’en suis pas tout à fait sûr, mais il semble:

     if assigned(object.owner) then object.free 

    fonctionne bien Dans cet exemple, ce serait

     if assigned(FBitmap.owner) then FBitmap.free