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

Jer*_*dge 57 delphi

这个问题是stackoverflow上人们特别评论的延续,我现在已经看过几次不同的时间了.我和教我Delphi的开发人员一样,为了保证安全,if assigned()在释放对象之前,以及在做其他各种事情之前总是先做检查.但是,我现在被告知我应该添加此支票.我想知道如果我这样做,应用程序编译/运行的方式是否存在任何差异,或者它是否会对结果产生影响...

if assigned(SomeObject) then SomeObject.Free;
Run Code Online (Sandbox Code Playgroud)

假设我有一个表单,我在表单创建时在后台创建一个位图对象,并在完成后释放它.现在我想我的问题是,当我试图访问可能在某些时候可能已经免费的对象时,我已经习惯了对我的很多代码进行检查.即使没有必要,我也一直在使用它.我喜欢彻底......

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.
Run Code Online (Sandbox Code Playgroud)

现在让我们说我正在引入一个名为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;
Run Code Online (Sandbox Code Playgroud)

在许多情况下,至少我希望在我尝试释放它之前仍然创建对象.但是你永远不知道未来在对象被释放之前会发生什么样的滑动.我总是使用这张支票,但现在我被告知我不应该这样,而且我仍然不明白为什么.


编辑

这是一个例子,试图向你解释为什么我有这样做的习惯:

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

我的观点是if SomeCreatedObject <> nil不一样的if Assigned(SomeCreatedObject),因为释放后SomeCreatedObject,其计算结果不是nil.所以这两项检查都是必要的.

Dav*_*nan 130

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

Assigned功能的含义

你问题中的大部分代码都背叛了对Assigned函数的错误理解.该文档的状态如下:

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

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

Assigned(P)对应于指针变量的测试P <> nil,对于程序变量对应于@P <> nil.

如果P 为零,则赋值返回False,否则为True.

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

....

注意:Assigned无法检测到悬空指针 - 也就是说,指针不是nil,但不再指向有效数据.

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

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

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

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

if obj<>nil
Run Code Online (Sandbox Code Playgroud)

if Assigned(obj)
Run Code Online (Sandbox Code Playgroud)

是完全可以互换的.

Assigned呼叫前测试Free

执行TObject.Free非常特殊.

procedure TObject.Free;
begin
  if Self <> nil then
    Destroy;
end;
Run Code Online (Sandbox Code Playgroud)

这允许您调用Free对象引用,nil这样做无效.对于它的价值,我知道RTL/VCL中没有其他地方可以使用这样的技巧.

您希望允许Freenil对象引用上调用的原因源于构造函数和析构函数在Delphi中的运行方式.

在构造函数中引发异常时,将调用析构函数.这样做是为了释放在成功构造函数的那一部分中分配的任何资源.如果Free没有实现,那么析构函数必须如下所示:

if obj1 <> nil then
  obj1.Free;
if obj2 <> nil then
  obj2.Free;
if obj3 <> nil then
  obj3.Free;
....
Run Code Online (Sandbox Code Playgroud)

拼图的下一部分是Delphi构造函数将实例内存初始化为零.这意味着任何未分配的对象引用字段都是nil.

把这一切放在一起,析构函数代码现在变成了

obj1.Free;
obj2.Free;
obj3.Free;
....
Run Code Online (Sandbox Code Playgroud)

您应该选择后一个选项,因为它更具可读性.

有一种情况需要测试是否在析构函数中分配了引用.如果你需要在破坏它之前调用该对象的任何方法,那么显然你必须防止它存在的可能性nil.因此,如果它出现在析构函数中,此代码将冒AV的风险:

FSettings.Save;
FSettings.Free;
Run Code Online (Sandbox Code Playgroud)

相反,你写

if Assigned(FSettings) then
begin
  FSettings.Save;
  FSettings.Free;
end;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

在这种情况下再没有需要测试AssignedTMyObject.Update.原因是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;
Run Code Online (Sandbox Code Playgroud)

在这种情况下,该类支持两种操作模式,包括和不使用日志记录.决定是在构造时进行的,任何引用日志记录对象的方法都必须测试它的存在.

这种不常见的代码形式使得Assigned对非可选对象不使用伪调用更为重要.当您if Assigned(FLogger)在代码中看到应该清楚地表明该类可以在FLogger不存在的情况下正常运行.如果你Assigned在你的代码周围喷射无偿的电话,那么你就无法一眼就知道对象是否应该永远存在.

  • David Heffernan是一个活生生的维基百科:)谢谢你的这个anser (7认同)
  • @kobik你100%正确.保证类的实例初始化为零.局部变量未初始化. (4认同)
  • @David,在`TMyObject.Destroy`中你调用`FLogger.Free`而不检查它是否是Assigned.是因为当`UseLogging`为False时,`TMyObject.Create`将始终将其初始化为nil?当在一个过程中声明一个本地TObject变量时,我们不能简单地调用object.Free而不首先初始化它.或者我错了? (3认同)

Joe*_*ite 22

Free有一些特殊的逻辑:它检查是否Selfnil,如果是这样,它返回而不做任何事情-这样你就可以安全地调用X.Free,即使Xnil.当你编写析构函数时,这很重要 - 大卫在他的答案中有更多的细节.

您可以查看源代码Free以了解其工作原理.我没有方便的Delphi源代码,但它是这样的:

procedure TObject.Free;
begin
  if Self <> nil then
    Destroy;
end;
Run Code Online (Sandbox Code Playgroud)

或者,如果您愿意,可以将其视为等效代码,使用Assigned:

procedure TObject.Free;
begin
  if Assigned(Self) then
    Destroy;
end;
Run Code Online (Sandbox Code Playgroud)

您可以编写自己的方法来检查if Self <> nil,只要它们是静态(即,不是virtualdynamic)实例方法(感谢David Heffernan的文档链接).但是在Delphi库中,Free我所知道的唯一方法是使用这个技巧.

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

  • 嗯,当然.如果你执行`x.Free`,那么`x`仍然指向过去的对象的内存地址,并且`x <> nil`和`Assigned(x)`将返回'True`.因此,如果你的变量没有立即超出范围,那么当你释放它所指向的对象时,将它设置为"nil"是一个好习惯.这就是"FreeAndNil"被发明的原因. (4认同)
  • 究竟是什么我说你扭曲的意思是"调用`x.Free`会改变`x`变量指向'nil`"?我清楚而具体地说过*相反.* (4认同)
  • @JerryDodge:听起来你有点确信`Assigned(I)`有一些神奇的能力可以检查`I`是否指向已经被释放的对象.它没有.就像我们一直告诉你的那样,`Assigned`检查`nil`.试试吧.`I:= TObject.Create; 我自由; 如果我<> nil然后ShowMessage('I <> nil'); 如果Assigned(I)则ShowMessage('Assigned(I)');`将显示两条消息:`I <> nil`和`Assigned(I)`.*那是因为两次检查完全相同.* (4认同)
  • 除非你使用委托类型(`object of procedure`或`function of object`),否则`Assigned`与检查`<> nil`完全相同*. (3认同)
  • @Jerry - Ctrl +在Joe的测试代码中点击`Assigned`,看看它是否带你到系统单元. (3认同)

Mar*_*ner 17

为什么你不应该打电话

if Assigned(SomeObject) then 
  SomeObject.Free;
Run Code Online (Sandbox Code Playgroud)

只是因为你会执行这样的事情

if Assigned(SomeObject) then 
  if Assigned(SomeObject) then 
    SomeObject.Destroy;
Run Code Online (Sandbox Code Playgroud)

如果你刚才打电话就是SomeObject.Free;这样

  if Assigned(SomeObject) then 
    SomeObject.Destroy;
Run Code Online (Sandbox Code Playgroud)

对于您的更新,如果您害怕对象实例引用,请使用FreeAndNil.它会破坏和取消引用你的对象

FreeAndNil(SomeObject);
Run Code Online (Sandbox Code Playgroud)

它就像你打电话一样

SomeObject.Free;
SomeObject := nil;
Run Code Online (Sandbox Code Playgroud)

  • 这个+10!本主题下的整个讨论归结为以下几点:"SomeObject.Free"通过调用析构函数链并释放分配的内存来销毁实例.它确实**不会改变`SomeObject`的值.因为它是一个指针,即一个内存地址,它将_still_承载那个相同的地址_after_`AsseObject.Free`而且,AFAIK,释放的内存没有填充空字节,因为`TObject.InitInstance`在构造时.所以,理想情况下,使用`FreeAndNil(SomeObject)`否则就没有机会告诉死亡的实例变量 (3认同)