Delphi FreeAndNil: 寻找替代实现

4
注意:请耐心等待,我因为一些关于这里这里的讨论以及我报告的这里这里的一些问题而感到有点“火烤”。
一些背景信息
古老的(10.4之前的)FreeAndNil看起来像这样:
FreeAndNil(var SomeObject)

新的和更新的FreeAndNil看起来像这样:

FreeAndNil(const [ref] SomeObject: TObject);

我认为两种方法都有其缺点:

  • 旧方法没有进行任何类型检查,因此在指针、记录和接口上调用FreeAndNil可以编译通过,但是在运行时会产生有趣但通常不想要的效果(完全失控或者幸运的话会停止并显示EAccessViolation、EInvalidOperation等错误信息)。
  • 新方法接受const参数,因此可以接受任何对象。但是提供的对象指针实际上是通过某些hacky-wacky代码更改的。
  • 现在可以像这样调用新的FreeAndNil: FreeAndNil(TObject.Create),它可以编译并且运行良好。我喜欢旧的FreeAndNil,它会在我犯错并提供属性而不是字段时警告我。不确定如果向这个FreeAndNil实现提供一个对象类型属性会发生什么。我没有尝试过。
如果我们将签名更改为FreeAndNil(var SomeObject:TObject),那么它将不允许我们传递除TObject类型之外的任何其他变量类型。这也是有道理的,因为如果不是FreeAndNil,则可以轻松地将作为类型TComponent提供的变量更改为完全不同类型的对象,例如TCollection。当然,FreeAndNil不会这样做,因为它总是将var参数更改为nil。

因此,这使得FreeAndNil成为一个特殊情况。甚至可能特别到足以说服Delphi添加一个编译器魔法FreeAndNil实现?有人投票吗?

潜在解决方法

我想出了下面的代码作为替代方案(这里作为辅助方法,但也可以是TObject实现的一部分),它有点结合了两个世界。 Assert将有助于在运行时查找无效调用。

procedure TSGObjectHelper.FreeAndNilObj(var aObject);
begin
  if Assigned(self) then
  begin
    Assert(TObject(aObject)=self,ClassName+'.FreeAndNil Wrong parameter provided!');
    pointer(aObject):=nil;
    Destroy;
  end;
end;

使用方法如下:

var MyObj:=TSOmeObject.Create;
...
MyObj.FreeAndNilObj(MyObj);

我已经测试了这个例程,它甚至比10.4的FreeAndNil实现略快。我猜是因为我首先进行赋值检查并直接调用Destroy。
但我不太喜欢的是:
- 类型检查发生在运行时,并且仅在启用断言时才会检查。 - 感觉就像必须两次传递相同的变量。这不一定是真的/必需的。它必须是同一个对象,并且参数必须是一个变量。
另一个调查
但如果可以不带参数调用,那不是很好吗?
var MyObj:=TSomeObject.Create;
...
MyObj.FreeAndNil;

我搞乱了`self`指针,并使用10.4中的`FreeAndNil`所使用的Hacky-Wacky代码将其设置为`nil`。嗯…这在方法内部有效,`self`指向`nil`。但是在像这样调用`FreeAndNil`之后,`MyObj`变量不是`nil`,而是一个过时的指针(这是我预期的)。此外,`MyObj`可以是属性或例程、构造函数等的结果。
所以这里也没有解决办法...
最后的问题是:你能想到更干净/更好的解决办法或技巧吗?
  • FreeAndNil(var aObject:TObject) 具有不太严格的类型检查编译时间(可能是编译器指令?),因此允许编译和调用任何对象类型的变量。
  • 当传递的内容不是某个对象类型的变量/字段时,会在编译时发出警告
  • 帮助描述RSP-29716中最佳解决方案/要求

2
不要一开始就使用FreeAndNil!如果您真的必须将变量设置为nil,请自行完成。 - Uwe Raabe
4
哎?FreeAndNill接收一个常量引用,但它还是将传入的引用设置为 nil?这个函数现在在说谎了。虽然以前它相当不安全(新函数也没更安全),但至少它没有撒谎。 - Allen Bauer
9
@AllenBauer 你会考虑回到 Embarcadero,清理/修复在你离开期间出现的问题吗? - Remy Lebeau
1
@AllenBauer Delphi有许多优秀的开发人员,但如果我能指出Anders Hejlsberg离开后对Delphi最大的损失,那就是你的离开。我完全同意Remy的看法...事情已经出了差错,需要修复很多问题...我不指望这会发生...但是如果你回来了,那将是无法言喻的美好。 - Dalija Prasnikar
2
实现RSP-29716是不可能的,因为一个var参数(既输入又输出)是不变的 - 请了解类型变化。像FreeAndNil一样将值视为协变的技巧只能起作用,因为输出值永远只会是nil而不是其他任何值。 - Stefan Glienke
显示剩余10条评论
2个回答

7
唯一既类型安全又不允许释放函数结果和属性的FreeAndNil适当解决方案是使用泛型var参数。
 procedure FreeAndNil<T: class>(var Obj: T); inline;

目前,Delphi编译器不允许在独立的过程和函数上使用泛型 https://quality.embarcadero.com/browse/RSP-13724

但这并不意味着您不能拥有通用的FreeAndNil实现,只是它会比必要的更加冗长。

type
  TObj = class
  public
    class procedure FreeAndNil<T: class>(var Obj: T); static; inline;
  end;

class procedure TObj.FreeAndNil<T>(var Obj: T);
var
  Temp: TObject;
begin
  Temp := Obj;
  Obj := nil;
  Temp.Free;
end;

Rio引入的类型推断机制使您在调用时无需指定泛型签名:

TObj.FreeAndNil(Obj);

在老版本的Delphi中,调用(和使用)通用的FreeAndNil也是可能的,但会变得更加冗长。
TObj.FreeAndNil<TFoo>(Obj);

啊,正是由于类型推理,我才能在我的TObjectHelper实现的SGFreeAndNil<T>中省略<T>。这允许在任何对象中使用我的SGFreeAndNil(FSomeField)。它涵盖了我代码约99%的部分,在这些代码中(所拥有的)对象被销毁。只有在全局例程(例如初始化/.finalization)中需要调用FreeAndNil的特殊调用,如TObject.FreeAndNil(MyGLobalObject)。 - H.Hasenack
1
由于全局例程缺乏通用的var,我建议在TObject类级别上实现class procedure TObject.FreeAndNil<T>(var aObject:T),并完全弃用全局FreeAndNil。这也避免了在TObject级别上需要帮助类的需要。只有在从全局例程调用时才需要使用TObject.FreeAndNil(SomeObjectVariable)。在所有对象方法中,它将简单地是FreeAndNil(SomeObject)。 - H.Hasenack
我对这个解决方案不太满意的地方在于它引入了一个你实际上并不需要或使用的新类。因此,我提供了下面的解决方案。对于当前的10.4版本,可以将其实现为“TObjectHelper =class helper for TObject”类;对于未来的版本,应将其添加到TObject类中。 - H.Hasenack

2

由于我们无法创建一个全局的procedure FreeAndNil<T:class>(var aObject:T),我建议以下代码作为TObject类的方法。(rtl更改由embarcadero进行,但不需要编译器更改)

class procedure TObject.InternalFreeAndNil(var Object:TObject); static; // strict private class method
begin
  if Assigned(Object) then
  begin
    var tmp:=Object;
    Object:=nil;
    tmp.Destroy;
  end;
end;


class procedure TObject.FreeAndNil<T:class>(var Object:T); inline; // public generic class method
begin
  InternalFreeAndNil(TObject(Object));
end;

为避免歧义,需要从sysutils单元中删除当前版本(10.4及之前版本)的FreeAndNil方法。

当在任何其他方法中调用新的泛型FreeAndNil方法时,可以简单地调用:

FreeAndNil(SomeObjectVariable)

而 10.3+ 类型推断避免了需要编写以下内容:

FreeAndNil<TMyClassSpec>(SomeObjectVariable)

这很好,因为大部分你的代码都可以不做改变就能顺利编译

在一些其他地方,例如全局例程和初始化/终止部分,需要调用:

TObject.FreeAndNil(SomeObjectVariable)

对我来说,这是可以接受的,比当前和历史上半成品的解决方案更好,以及具有FreeAndNil(const [ref] aObject:TObject)或无类型FreeAndNil(var aObject)

由于该例程非常简单且性能似乎是一个问题,因此可以提出使用汇编实现它的论点。 虽然我不确定是否允许/可能用于通用(并最好是inline)方法。

FTM:也可以保留FreeAndNil(var aObject:TObject)并告诉人们进行下面的类型转换,这也避免了编译器对变量类型的抱怨。 但在这种情况下,可能需要调整大量源代码。 另一方面,它节省了代码膨胀,仍然避免了将函数结果、属性或无效类型(如记录和指针)作为FreeAndNil参数的无效使用,并且易于更改/实施。

...
var Obj:=TSomeObject.Create;
try
   DoSOmethingUseFulWithObj(Obj);
finally
  FreeAndNil(TObject(Obj)); // typecast avoids compiler complaining. Compiler wont allow invalid typecasts
end;
...

虽然在编写代码时你可以按照自己的喜好进行操作,但我不喜欢使用类助手或将FreeAndNil添加到RTL中的TObject类。类助手并不理想,因为你只能在作用域内使用一个,所以我不喜欢在这样的全局级别上使用它们,因为可能会与另一个发生冲突。官方RTL实现TObject类会破坏向后兼容性,而收益却不大。 - Dalija Prasnikar
@DalijaPrasnikar 你能详细说明一下 Foo.FreeAndNil(Bar) 会出现什么问题或潜在的错误吗?它只会简单地销毁 Bar(如果有的话),并将其设置为 nil。从技术上讲,它等同于 TFoo.FreeAndNil(Bar),因为它是一个类过程。它依赖于类型推断和泛型来获取正确的 TObject.FreeAndNil<TInferredType>(var aObject:TInferredType)。我同意一个全局的 FreeAndNil<TInferredType> 会是最好的选择,但这需要编译器的更改。RTL源代码的更改似乎比编译器的更改更有可能发生,但谁知道呢。 - H.Hasenack
如果你真的想释放Foo而不是Bar,那么事情可能会出错。如果你读代码的速度足够快,你很容易忽略实际参数Bar,因为在阅读时Foo更加突出。 - Dalija Prasnikar
@DalijaPrasnikar:我认为Foo.FreeAndNil(Bar)确实令人困惑,应该首先写成TFoo.FreeAndNil(Bar)。TObj确实是任意的,我喜欢TObject,因为1)我们可以避免使用类助手。2)由于它是一个类方法(而不是常规方法),它允许替换现有的FreeAndNil,并且不会对虚拟表产生影响。3)我不认为TObject接口的更改会破坏任何现有的对象相关代码。但这可能是我Delphi经验不足造成的。 - H.Hasenack
"应该"和"现实"是有区别的。这就是第一次RTL的FreeAndNil实现存在问题的原因,也是第二个实现存在问题的原因。我强烈反对在TObject.FreeAndNil<T>方面向RTL添加另一个不成熟的解决方案,下一个RTL应该是一个正确的独立通用解决方案。 - Dalija Prasnikar
显示剩余5条评论

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