如何将nil传递给变量参数?

7

有很多 API 例程需要以指针形式作为参数传递某些变量,这些例程被翻译为 var 参数,但根据 Windows 帮助文件可以将它们指定为 nil 指针。

例如,ChangeDisplaySettings 函数声明如下:

function ChangeDisplaySettings(var lpDevMode: TDeviceMode; dwFlags: DWORD): Longint; stdcall;

但 Windows 帮助文件明确说明:“将 lpDevMode 参数传递为 NULL 是在动态模式更改后返回默认模式的最简单方法。” 正确的翻译应该是:

function ChangeDisplaySettings(lpDevMode: PDeviceMode; dwFlags: DWORD): Longint; stdcall;

我发布这个问题和答案是为了帮助新手解决这些问题,而无需重新声明函数。我仍然记得在一开始时这是一个问题。

重新声明函数是正确的解决方案。 - David Heffernan
@David,谢谢;我尊重您作为Delphi明显经验丰富的老手的意见。然而,就个人而言,我不喜欢在各处重新声明标准例程,而更喜欢在可以的地方使用标准例程。不过,我会更新我的答案以表明标准做法似乎更倾向于重新声明。 - Jannie Gerber
1
应该有两个重载函数。一个使用 var,另一个使用指向结构体的指针。然后调用者可以选择使用哪一个。 - David Heffernan
2个回答

13

一个解决方案是使用指针代替var参数重新声明任何这样的函数,但有一个更简单的解决方案。只需将取消引用的nil指针强制转换为正确的类型,例如对于ChangeDisplaySettings示例,请使用以下内容将显示模式重置为默认注册表设置:

ChangeDisplaySettings(TDeviceMode(nil^), 0);
或者
ChangeDisplaySettings(PDeviceMode(nil)^, 0);

这种方式传递了一个位于内存地址零的变量参数 - 编译器很高兴,你可以将一个空指针传递给API例程!

编辑:根据David Hefferman的评论,似乎标准做法是重新声明这样的例程。个人而言,我喜欢在我的个人单元中使用标准声明,但出于专业工作目的,建议开发人员遵循标准实践。


3
我个人认为你的第二个选项更自然。 - Andreas Rejbrand
@Andreas,我也更喜欢使用第二种方式,但列出了第一种方式,以防指针类型未声明的情况。另一方面,David Hefferman 倾向于重新声明,这是唯一的解决方案,当 Delphi 的新版本修复标准声明时不会导致错误! - Jannie Gerber
@Jannie - 这种解决方案会出现问题的另一种方式是,如果编译器被更改以正确地识别这种类型转换技巧破坏参数合同并将代码拒绝为无效,则该解决方案将失效。在这种情况下提供的声明仅作为副作用是正确的。不要管“标准做法”,正确的解决方案是使用正确的声明,而不是设计一个副作用来抵消另一个副作用。 - Deltics
1
@Deltics,我同意正确的解决方案是正确的声明,但我严重怀疑编译器会像你描述的那样改变。将内存地址强制转换为某种类型的指针,然后对其进行解引用,对于var参数来说是完全有效的。在这种情况下,我们只是使用nil(地址0)。当然,nil在Delphi中有特殊的含义,但我仍然怀疑他们会开始禁止某些值,以防止程序员以非标准的方式使用语言。 - Jannie Gerber
@Jannie 我有10.2东京版本。 - Molochnik
显示剩余3条评论

6
除了其他有帮助的答案和评论,我还有另一个观点。在这种情况下,将API从头文件翻译过来的人没有仔细查看API文档。如果他们这样做了,就会清楚地知道传递"nil"是有效的。
在这种情况下,正确的做法是声明一组重载,这些重载都引用相同的导入。一个是好的"var"参数版本,另一个是"指向结构体"版本。这样,您就可以直接传递TDeviceMode变量(无需取变量的地址),并在必要时仍然传递nil。编译器会将"nil"与指向结构体匹配,然后引用它。由于两个API解析为相同的API,并且参数传递的实际方式没有区别,因此一切仍然按预期工作。
由于在应该能够传递"nil"的情况下没有重载版本的API,这是API翻译错误。请随意在http://quality.embarcadero.com上报告此问题。
记录上,我在产品中进行了很多API翻译...可以想象我是那个不做适当研究的愚蠢开发人员:)。

个人认为,“好的var参数版本”已经过时了。好吧,自Borland Pascal时代以来一直在使用,但实际上,我更喜欢更接近原始代码的翻译,即始终使用指针。这样做的好处是API文档(几乎总是基于C的)不会在Delphi中使用“var”时引起任何混淆(最小惊奇原则)。文档显示一个指针,翻译也有一个指针。没有混淆,不需要仔细阅读文档。只有对于已经存在的翻译才需要重载。 - Rudy Velthuis
1
此外,如果一个函数没有被标记为重载,那么就无法声明其重载。因此,你要么修改原始导入单元(这并不总是可行的),要么隐藏原始函数,并在自己的单元中声明两个重载函数,其中一个是精确重新声明。 - Rudy Velthuis
这就是为什么我建议在质量门户中报告它。我从未建议过在原始文件没有被标记为超载的情况下进行操作。 - Allen Bauer
好的。我实际上是在试图说服你和Embarcadero放弃“尽可能使用var而不是指针类型”的原则。<g> - Rudy Velthuis
@Allen,不幸的是,我目前只有Delphi 7;所以我无法检查它是否仍然不正确。另一个是IShellFolder.ParseDisplayName,在这种情况下,如果不需要,dwAttributes可以是nil。这个问题并不严重;只需提供一个虚拟变量即可。翻译错误不会影响任何功能。 - Jannie Gerber
显示剩余2条评论

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