如何安全地绕过Delphi错误:“形式和实际参数的类型必须相同”

6

我需要一种编写通用程序的方法,以便对一个对象类型或其任何子类进行操作。

我的第一次尝试是声明

procedure TotalDestroy(var obj:TMyObject);

但是当与一个子对象一起使用时

type TMyNewerObject = class(TMyObject);
var someNewerObject: TMyNewerObject;

TotalDestroy(someNewerObject);

我得到了臭名昭著的错误“形式参数和实际参数的类型必须相同”。所以在努力寻找解决方案时,我查看了Delphi系统FreeAndNil过程的源代码。我发现这个声明非常棒,还有一个惊人的注释。
{ FreeAndNil frees the given TObject instance and 
  sets the variable reference to nil.  
  Be careful to only pass TObjects to this routine. }

procedure FreeAndNil(var Obj);

它避免了类型检查错误,但没有使用安全网。

我的问题是......有没有安全的方式来检查未命名变量参数的类型?

换句话说,您能否改进此Delphi源代码,以便不需要警告?

procedure FreeAndNil(var Obj);
var
  Temp: TObject;
begin
  Temp := TObject(Obj);
  Pointer(Obj) := nil;
  Temp.Free;
end;

4
如果有一种方法可以完成这个过程,你不觉得这个步骤本来就应该被写成不同的形式吗? - Lasse V. Karlsen
2
我不知道,所以我在问。 - PA.
3
PA,对于Lasse的问题,答案是“是”。如果有更好的方法,那么库函数一定会使用它。由于库函数没有使用它,我们可以安全地得出结论:没有更好的方法。 - Rob Kennedy
1
@Rob: “safely conclude”? 你是在说 Delphi 团队是不可错误的吗?尽管我很喜欢 Delphi,但我并不相信那个。 :-) - Uli Gerhardt
2
我认为这是一种否定前提谬误,你的意思是“如果Delphi团队想到了修复方法,并且有时间修复它,而且它比系统中其他已知错误更重要,他们就会修复它”,因此这并不是一个完全正确的结论。 - Zartog
显示剩余8条评论
4个回答

11

让我们来看看您想要做什么。

您想调用一个需要X的方法,并传入一个类型为Y的对象,其中Y是X的后代。问题在于,该参数是“var”参数。

如果可能的话,让我们分析一下您可以做什么。

type
    TBase = class
    end;
    TDescendant = class(TBase)
    end;

procedure Fiddle(var x: TBase);
begin
    x := TDescendant.Create;
end;

type
    TOtherDescendant = class(TBase)
    end;

var a: TOtherDescendant;
a := TOtherDescendant.Create;
Fiddle(a);
哎呀,现在a不再包含TOtherDescendant的实例,它包含了TDescendant的实例。这可能会让随后的调用代码感到意外。
你不仅要考虑你打算使用的语法,还要考虑你实际上可以使用的语法。
你应该阅读Eric Lippert关于.NET类似问题的博客文章,可以在此处找到:Why do ref and out parameters not allow type variation?

感谢您的见解和提供链接。正如我在下面评论中所述,我的意图是最终将引用设置为nil。 - PA.

7

我之前写过类似于Lasse的例子,与这个内容相关:

除非你要编写一个赋值语句来更改输入参数本身的值,而不仅仅是其中一个属性,否则你不应该首先通过引用传递参数。

如果你正在编写一个赋值语句来更改参数的值,那么编译器的消息确实是正确的,你应该遵循它。

需要绕过错误的一个原因是当你编写像TApplication.CreateForm这样的函数时。它的工作是更改输入参数的值,新值的类型因素而异,无法在编译时确定。如果你正在编写这样的函数,那么在Delphi中你唯一的选择是使用一个未类型化的变量参数,然后调用者和接收者都需要确保一切顺利。调用者需要确保传递一个能够容纳函数将放入其中任何类型值的变量,而函数需要确保存储与调用者请求的兼容类型的值。

CreateForm的情况下,调用者传递类引用文字和该类类型的变量。函数实例化类并将引用存储在变量中。

我对CreateFormFreeAndNil都不是很看好,主要是因为它们的无类型参数为了相对较少的额外便利而牺牲了类型安全性。你还没有展示你的TotalDestroy函数的实现,但我怀疑它的var参数最终会像那两个函数一样提供同样低的效用。请参阅我的这两篇文章:


正如你聪明地猜到的那样,我的TotalDestroy对TMyObject进行了一些特殊的清理,并最终执行了FreeAndNil操作。 - PA.
3
如果需要对TMyObject进行特殊清理,请考虑将其放在析构函数中。不要强制类的使用者学习新的销毁方式,当普通的Free对其他所有情况都适用时。 - Rob Kennedy
谢谢Rob。我明白你的意思了,我不需要我的TotalDestroy。我将只使用我已经拥有的Destroy析构函数。并让用户通过FreeAndNil决定是否需要它。 - PA.

3
除了Lasse所写的内容(非常正确),大多数情况下你不需要将对象传递给一个var参数。
对象是引用类型。你看到的对象实际上是对它的引用。如果你只想修改对象的成员,那么你可以将其简单地传递给一个普通参数。让方法调用采用TMyObject参数而不是var TMyObject参数,它应该可以工作。
当然,如果你真的要替换对象,那么请忽略所有这些,并查看Lasse的答案。

同意,我没有考虑到那个角度。非常正确,如果你只想要引用,就不需要使用“var”。 - Lasse V. Karlsen
实际上,我只是想将其置为nil,并在此之前执行一些特殊的清理工作。 - PA.

1

你能否改进这段Delphi源代码,使得不再需要警告?

是的,你可以通过一种类型安全的方式避免编译器错误。 在最新的Delphi 10.4 Sidney中,FreeAndNil过程已经被改成了这样:

procedure FreeAndNil(const [ref] Obj: TObject);
var
  Temp: TObject;
begin
  Temp := Obj;
  TObject(Pointer(@Obj)^) := nil;
  Temp.Free;
end;

它对对象是类型安全的,当传递接口引用时会捕获错误。

通过 const [ref] 传递参数意味着参数是通过引用传递的。如果没有 [ref] 属性,则大小等于或小于指针的参数将通过值传递。

在这里,即使对象被作为常量传递,引用也会被修改。从这个意义上说,它不是一个完美的声明,但比以前的实现更好地完成了其工作。

来自 Delphi 10.4中的新功能

这意味着错误使用FreeAndNil将导致编译器错误。过去,不正确的用法不会被捕获,导致难以处理的bug。请注意,尽管参数声明为const,但按引用传递的变量确实被修改了。
在这种FreeAndNil声明下,可能出现一种新的、但“不那么糟糕”的不正确调用类别:可以调用该方法传递属性或方法结果,以及强制转换表达式、类型隐式转换为TObject等。nil值将是表达式中的临时变量。

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