为啥我不应该在访问对象之前使用“if Assigned()”?

Posted

技术标签:

【中文标题】为啥我不应该在访问对象之前使用“if Assigned()”?【英文标题】:Why should I not use "if Assigned()" before accessing objects?为什么我不应该在访问对象之前使用“if Assigned()”? 【发布时间】:2012-01-22 20:18:27 【问题描述】:

这个问题是人们对 *** 的特定评论的延续,我现在已经看过几次了。我和教我 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 &lt;&gt; nilif Assigned(SomeCreatedObject) 不同,因为在释放SomeCreatedObject 后,它不会评估为nil。所以这两项检查都应该是必要的。

【问题讨论】:

assigned(I)I &lt;&gt; nil 有何不同? (请注意,我根本不使用 Delphi :p~) @pst, Assigned 在大多数情况下与&lt;&gt; nil 完全相同。例外情况是事件,其中Assigned 有一点黑魔法来解决表单设计器中可能出现的问题(因此您总是需要使用Assigned 来检查事件是否分配,而对于其他任何东西 Assigned&lt;&gt; nil 是等效的)。 不,它们通常是同一个意思。唯一的区别是如果F是一个返回指针的函数变量,Assigned(F)检查F本身是否是nil,而F &lt;&gt; nil检查F的结果。 @JerryDodge,您编辑中的示例实际上并没有解释任何内容。你想做什么? @Jerry Dodge - 还可以考虑使用 FreeAndNil() 而不是 Free。对你有很大帮助!!!! 【参考方案1】:

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

Assigned函数的含义

您问题中的大部分代码都暴露了对Assigned 函数的错误理解。 documentation 声明如下:

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

使用Assigned来判断是指针还是程序 P 引用的是nil。 P 必须是指针的变量引用或 程序类型。

Assigned(P)对应测试Pnil为一个指针变量, @P nil 表示过程变量。

如果 P 为 nil

Assigned 返回 False,否则返回 True

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

....

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

Assigned 的含义因指针变量和过程变量而异。在此答案的其余部分中,我们将仅考虑指针变量,因为这是问题的上下文。请注意,对象引用是作为指针变量实现的。

从文档中获取的关键点是,对于指针变量:

    Assigned 相当于测试&lt;&gt; nilAssigned 无法检测指针或对象引用是否有效。

在这个问题的上下文中,这意味着

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 constructors initialise the instance memory to zero。这意味着任何未分配的对象引用字段都是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;

在这种情况下,再次无需在TMyObject.Update 中测试Assigned。原因是您根本无法调用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,那么您将无法一目了然地判断一个对象是否应该始终存在。

【讨论】:

@David,在TMyObject.Destroy 中,您正在调用FLogger.Free,而不检查它是否已分配。那是因为当UseLogging 为假时TMyObject.Create 将始终将其初始化为零?当在过程中声明一个局部 TObject 变量时,我们不能简单地调用 object.Free 而不首先对其进行初始化。还是我错了? @kobik 你是 100% 正确的。保证一个类的实例是零初始化的。局部变量未初始化。 @David Heffernan。是的,我考虑添加“这可能非常令人困惑”。但关键是答案是错误的:你可以在示例 tmyobject 或声明但未创建为 tmyobject 的对象上调用示例 .update,并且因为它测试 IsAssigned,所以 .update 的操作是定义,而不是“任何人的猜测”。 @David Heffernan 并不是说​​我想大谈特谈,但答案仍然是“除非 TMyObject 的构造函数成功,否则您根本无法调用 TMyObject.Update”。我同意您的观点,即您假设 FSettings 声明并不意味着 FSettings 为 NIL:因为问题是关于 IsAssigned 的使用,所以我做出了相反的假设。 David Heffernan 是一个活生生的*** :) 感谢这个分析器【参考方案2】:

我不完全确定,但似乎:

if assigned(object.owner) then object.free 

工作正常。在这个例子中它是

if assigned(FBitmap.owner) then FBitmap.free

【讨论】:

不,它仍然不起作用。我的意思是有时它有效,有时它不...如上所述。 但另外测试“object nil” - 也没有解决问题......我更喜欢使用另一个变量 fex。 “ifcreated_object:布尔值”。好吧,它工作正常。【参考方案3】:

Free 有一些特殊的逻辑:它检查Self 是否为nil,如果是,它不做任何事情就返回——所以即使X 是@,你也可以安全地调用X.Free 987654329@。这在您编写析构函数时很重要——David 在his answer 中有更多详细信息。

您可以查看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 &lt;&gt; nil,只要它们是 static (i.e., not virtual or dynamic) instance methods(感谢 David Heffernan 提供的文档链接)。但在 Delphi 库中,Free 是我所知道的唯一使用此技巧的方法。

所以在调用Free之前不需要检查变量是否为Assigned;它已经为您做到了。这就是为什么建议调用Free 而不是直接调用Destroy 的原因:如果你在nil 引用上调用Destroy,你会遇到访问冲突。

【讨论】:

除非您使用委托类型(procedure of objectfunction of object),否则Assigned完全与检查 &lt;&gt; nil 相同。跨度> 当然可以。如果你做x.Free,那么x仍然指向对象曾经所在的内存地址,x &lt;&gt; nilAssigned(x)都会返回True。因此,如果您的变量没有立即超出范围,那么当您释放它指向的对象时,将其设置为nil 是一个好习惯。这就是 FreeAndNil 被发明的原因。 我到底说了什么,你在扭曲的意思是“调用x.Free 会改变x 变量指向nil”?我已经清楚明确地说了相反。 @JerryDodge:听起来您似乎确信Assigned(I) 具有某种神奇的能力来检查I 是否指向已被释放的对象。它没有。就像我们一直告诉你的那样,Assigned 会检查 nil。试试吧。 I := TObject.Create; I.Free; if I &lt;&gt; nil then ShowMessage('I &lt;&gt; nil'); if Assigned(I) then ShowMessage('Assigned(I)'); 将显示两条消息:I &lt;&gt; nilAssigned(I)那是因为这两个检查做的事情完全相同。 @Jerry - Ctrl+点击 Joe 的测试代码中的Assigned,看看它是否带你到系统单元。【参考方案4】:

为什么你不应该打电话

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;

【讨论】:

为此+10!该主题下的整个讨论归结为以下几点:SomeObject.Free 通过调用析构函数链并释放分配的内存来销毁实例。它不会更改SomeObject 的值。由于它是一个指针,即一个内存地址,它仍然承载相同的地址之后 SomeObject.Free此外,AFAIK,释放的内存没有被空字节填充正如TObject.InitInstance 在施工时所做的那样。所以,理想情况下,使用FreeAndNil( SomeObject ) 否则没有机会区分活着的实例变量和死的实例变量

以上是关于为啥我不应该在访问对象之前使用“if Assigned()”?的主要内容,如果未能解决你的问题,请参考以下文章

为啥要撤销访问令牌?

为啥我不应该修改 UINavigationController 的工具栏?

为啥我不应该使用 Unity?

为啥我不应该在 IDP 上下文中使用 IdToken 作为不记名令牌?

为啥有人应该在 git commit 之前使用 git add?或者为啥有人应该使用 git add 呢?

为啥我不应该包含 cpp 文件而使用标头?