为什么在访问对象之前不应使用"if Assigned()"?

59

这个问题是从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 <> nilif Assigned(SomeCreatedObject) 是不同的,因为在释放 SomeCreatedObject 后,它不会被评估为 nil。因此,两个检查都应该是必要的。


16
在大多数情况下,“Assigned”与“<> nil”完全相同。例外是在事件中,“Assigned”具有一些黑色魔法,以解决在表单设计器中可能出现的问题(因此您始终需要使用“Assigned”来检查事件是否已分配,而对于其他任何内容,“Assigned”和“<> nil”是等效的)。 - Joe White
15
不,它们通常意思相同。唯一的区别是当“F”是返回指针的函数变量时,“Assigned(F)”检查“F”本身是否为“nil”,而“F <> nil”则检查“F”的结果。 - user743382
1
@JerryDodge,你编辑的示例并没有真正解释任何东西。你想做什么? - Joe White
3
还要考虑使用FreeAndNil()而不是Free。这会帮助你很多!!! - Gabriel
@Altar 实际上我会在它能够发挥好作用的地方使用它(主要是在指针可能稍后被读取的地方)。 - Jerry Dodge
显示剩余18条评论
5个回答

139

这是一个非常广泛的问题,有许多不同的角度。

Assigned函数的含义

你问题中的大部分代码表明了你对Assigned函数的错误理解。documentation中指出:

测试nil(未分配)指针或过程变量。

使用Assigned来确定指针或过程是否为nil。P必须是指针或过程类型的变量引用。

Assigned(P)对于指针变量的测试相当于P <> nil,对于过程变量的测试相当于@P <> nil

Assigned返回False如果P是nil,否则返回True

提示:当测试对象事件和过程是否被分配时,您不能测试nil,使用Assigned是正确的方法。

....

注意Assigned无法检测悬空指针--即不是nil,但不再指向有效数据的指针。

< p > Assigned 对指针和过程变量的含义有所不同。在本答案的其余部分中,我们将仅考虑指针变量,因为这是问题的上下文。请注意,对象引用被实现为指针变量。

从文档中获取的关键点是:

  1. Assigned 相当于测试 <> nil
  2. Assigned 无法检测指针或对象引用是否有效。

在这个问题的背景下,这意味着

if obj<>nil

并且

if Assigned(obj)

这两者是完全可以互换的。

在调用Free之前测试Assigned

TObject.Free的实现非常特殊。

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

这使您可以在对象引用为“nil”时调用Free,并且这样做没有任何效果。值得一提的是,我不知道RTL / VCL中是否还有其他地方使用这种技巧。
允许在nil对象引用上调用Free的原因源于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;
....

你应该选择后者,因为它更易读。
有一种情况需要测试引用是否在析构函数中分配。如果需要在销毁对象之前调用任何方法,则必须明确防范可能为空的情况。因此,如果此代码出现在析构函数中,则会存在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;

在这种情况下,再次无需在TMyObject.Update中测试Assigned。原因是,除非TMyObject的构造函数成功,否则您根本不能调用TMyObject.Update。如果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调用,则会失去一目了然地判断对象是否应始终存在的能力。

3
@David,在TMyObject.Destroy中,你调用了FLogger.Free,但没有检查它是否已经被分配。这是因为当UseLogging为False时,TMyObject.Create总是将其初始化为nil吗?在过程中声明一个本地的TObject变量时,我们不能简单地调用object.Free而不先初始化它。或者我错了吗? - kobik
4
@kobik 你说得完全正确。一个类的实例保证进行零初始化,而局部变量则未经初始化。 - David Heffernan
1
@David Heffernan。是的,我考虑过添加“可能非常令人困惑”的内容。但关键是答案是错误的:您可以在示例tmyobject或声明但未创建为tmyobject的对象上调用示例.update,并且由于它测试了IsAssigned,.update的操作是定义好的,而不是“任何人的猜测”。 - david
1
@David Heffernan,不是我想一直强调这个问题,但答案仍然是“除非TMyObject的构造函数成功,否则您无法调用TMyObject.Update”。我理解你的观点,即你假设FSettings的声明并没有意味着FSettings为NIL:由于问题涉及IsAssigned的使用,我做出了相反的假设。 - david
8
David Heffernan 是一个活生生的维基百科 :) 感谢他提供的答案。 - mca64
显示剩余16条评论

23

Free有一些特殊的逻辑:它会检查Self是否为nil,如果是,则直接返回而不执行任何操作--因此你可以安全地调用X.Free,即使Xnil。这对于编写析构函数非常重要--David在他的答案中有更多细节。

你可以查看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,只要它们是静态(即非虚拟或动态)实例方法(感谢David Heffernan提供的文档链接)。但在Delphi库中,Free是我知道使用此技巧的唯一方法。

因此,在调用Free之前,您不需要检查变量是否已Assigned,因为它已经为您执行了检查。这实际上就是为什么建议调用Free而不是直接调用Destroy:如果在nil引用上调用Destroy,您将得到访问冲突。


3
除非您正在使用委托类型(procedure of objectfunction of object),否则Assigned与检查<> nil完全相同。 - Joe White
4
当你使用 x.Free 释放某个对象时,x 仍指向原来对象所在的内存地址,因此 x <> nilAssigned(x) 都返回 True。所以,如果你的变量不会立即超出其作用域,则在释放它所指向的对象时将其设置为 nil 是一种好习惯。这就是为什么发明了 FreeAndNil 的原因。 - Joe White
4
我到底说了什么,让你曲解成“调用x.Free会将x变量指向nil”?我明确而具体地说了相反的话 - Joe White
4
@JerryDodge:听起来你似乎相信Assigned(I)有一些神奇的能力,可以检查I是否指向已经被释放的对象。它没有这个能力。就像我们一直告诉你的那样,Assigned检查的是nil。试试看。I := TObject.Create; I.Free; if I <> nil then ShowMessage('I <> nil'); if Assigned(I) then ShowMessage('Assigned(I)');会显示两条消息:I <> nilAssigned(I)这是因为两个检查做的事情完全相同。 - Joe White
3
@Jerry - 在Joe的测试代码中按Ctrl+单击 Assigned,看它是否会跳转到系统单元。 - Sertac Akyuz
显示剩余18条评论

21

为什么你不应该调用(某个函数)

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;

5
此主题下的整个讨论归结为以下一点: SomeObject.Free 通过调用析构函数链和释放分配的内存来销毁实例。它不会改变SomeObject的值。由于它是一个指针,即一个内存地址,在SomeObject.Free之后它仍将保留相同的地址。 此外,据我所知,释放的内存不会像TObject.InitInstance在构造时填充为 null 字节。 因此,最好使用 FreeAndNil(SomeObject) 否则将无法区分活动实例变量和已释放的实例变量。 - Semanino

0

我创建了一个简单的示例来展示我的过程:

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  FMX.Controls.Presentation, FMX.StdCtrls, FMX.Objects;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  Cir : TCircle;

implementation

{$R *.fmx}

procedure TForm1.Button1Click(Sender: TObject);
begin
  if Assigned(Cir) then
  begin
    Cir.Free;
    Cir := Nil;
  end;
  Cir := TCircle.Create(nil);
  with Cir do
  begin
    Parent := Form1;
    Height := 50;
    Width := 50;
    Position.X := 100;
    Position.Y := 100;
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  if Assigned(Cir) then
  begin
    Cir.Free;
    Cir := Nil;
  end;
end;

end.

这个回答如何回答问题?你是在尝试提出一个新问题吗? - Jerry Dodge

-4

我不完全确定,但似乎是:

if assigned(object.owner) then object.free 

运行良好。在这个例子中,它将是

if assigned(FBitmap.owner) then FBitmap.free

不行,它还是不工作。我的意思是有时候它能工作,有时候又不能……就像之前一样。 - Mariusz980
但是测试“object <> nil”也没有解决问题...我更喜欢使用另一个变量,例如“ifcreated_object:boolean”。好吧,它运行良好。 - Mariusz980

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接