18

当用 Delphi 2009 编译并运行时,这个控制台应用程序写“奇怪”。“小于”运算符两边的值相等,但代码表现得好像它们不相等。我能做些什么来避免这个问题?

program Project5;

{$APPTYPE CONSOLE}

var
  C: Currency;
begin
  C := 1.32;

  if C < 1.32 then
  begin
    WriteLn('strange');
  end;

  ReadLn;
end.

ps 代码适用于其他值。

Barry Kelly 的这个回答解释说 Currency 类型“不像浮点代码那样容易受到精度问题的影响”。

4

4 回答 4

9

这似乎是德尔福的回归。

Delphi 2010 中的输出是“奇怪的”。但在 XE2 中没有输出,因此该错误不存在。我手头没有 XE 可供测试,但感谢@Sertac 确认 XE 也输出“奇怪”。请注意,旧版本的 Delphi 也很好,所以这是 D2009 左右的回归。

2010年生成的代码是:

Project106.dpr.10: if C < 1.32 then
004050D6 DB2D18514000     fld tbyte ptr [$00405118]
004050DC DF2D789B4000     fild qword ptr [$00409b78]
004050E2 DED9             fcompp 
004050E4 9B               wait 
004050E5 DFE0             fstsw ax
004050E7 9E               sahf 
004050E8 7319             jnb $00405103
Project106.dpr.12: WriteLn('strange');

文字 1.32 存储为一个 10 字节的浮点值,其值应为 13200。这是一个可精确表示的二进制浮点值。存储为 10 字节浮点数的 13200 的位模式为:

00 00 00 00 00 00 40 CE 0C 40

但是,存储在 $00405118 的文字中的位模式是不同的,并且略大于13200. 值为:

01 00 00 00 00 00 40 CE 0C 40

这就解释了为什么C < 1.32评估为True.

在 XE2 上生成的代码是:

Project106.dpr.10: if C < 1.32 then
004060E6 DF2DA0AB4000     fild qword ptr [$0040aba0]
004060EC D81D28614000     fcomp dword ptr [$00406128]
004060F2 9B               wait 
004060F3 DFE0             fstsw ax
004060F5 9E               sahf 
004060F6 7319             jnb $00406111
Project106.dpr.12: WriteLn('strange');

请注意,文字保存在 4 字节浮点数中。这可以从我们比较的事实中看出dword ptr [$00406128]。如果我们查看存储在的单精度浮点数的内容,$00406128我们会发现:

00 40 4E 46

这正是 13200 表示为 4 字节浮点数。

我的猜测是,2010 年的编译器在遇到以下问题时会执行以下操作1.32

  • 将 1.32 转换为最接近的可精确表示的 10 字节浮点数。
  • 将该值乘以 10000。
  • 将生成的 10 字节浮点数存储在$00405118.

因为 1.32 不能完全表示,所以最终的 10 字节浮点数并不完全是 13200。并且推测当编译器从将这些文字存储在 4 字节浮点数中切换到将它们存储在 10 字节浮点数中时,就会出现回归。

根本问题是德尔福对Currency数据类型的支持是建立在一个完全有缺陷的设计之上的。使用二进制浮点运算来实现十进制定点数据类型简直是自找麻烦。修复设计的唯一合理方法是完全重新设计编译器以使用定点整数运算。令人失望的是,新的 64 位编译器使用与 32 位编译器相同的设计。

老实说,我会阻止 Delphi 编译器对Currency文字进行任何浮点运算。这只是一个完整的雷区。我会像这样在脑海中进行 10,000 次转变:

function ShiftedInt64ToCurrency(Value: Int64): Currency;
begin
  PInt64(@Result)^ := Value;
end;

然后调用代码将是:

C := 1.32;
if C < ShiftedInt64ToCurrency(13200) then
  Writeln ('strange');

编译器没有办法把它搞砸!

哼!

于 2013-01-16T14:38:48.160 回答
5

像 Currency(1.32) 这样的硬转换是不可能的,您可以使用以下内容进行显式转换

Function ToCurrency(d:Double):Currency;
    begin
       Result := d;
    end;

procedure TForm1.Button1Click(Sender: TObject);

var
  C: Currency;

begin
  C := 1.32;
  if C < ToCurrency(1.32) then
  begin
    Writeln ('strange');
  end;
end;

另一种方法可以通过使用 const 或变量来强制使用 curreny

const
  comp:Currency=1.32;
var
  C: Currency;
begin
  C := 1.32;
  if C < comp then
  begin
    writeln ('strange');
  end;
end;
于 2013-01-16T13:18:22.740 回答
2

为避免此问题(编译器中的错误),您可以按照@bummi 的建议进行操作,或者尝试以下运行时强制转换:

if C < Currency(Variant(1.32)) then

为避免往返 FPU(和舍入误差),请考虑使用此比较函数:

function CompCurrency(const A,B: Currency): Int64;
var
  A64: Int64 absolute A; // Currency maps internally as an Int64
  B64: Int64 absolute B;
begin
  result := A64-B64;
end;
...
if CompCurrency(C,1.32) < 0 then
begin
  WriteLn('strange');
end;

有关更多信息,请参阅此页面Floating point and Currency fields

于 2013-01-16T16:13:37.387 回答
0

添加到大卫的答案 - 下一个代码并不奇怪,虽然相当于 OP 代码:

program Project2;

{$APPTYPE CONSOLE}

var
  I: Int64;
  E: Extended;

begin
  I:= 13200;
  E:= 13200;
  if I < E then
  begin
    WriteLn('strange');
  end;
  ReadLn;
end.

现在编译器为 Extended(13200) 生成正确的二进制值,所以问题似乎与CurrencyDelphi 编译器中的错误类型实现有关。

于 2013-01-16T17:19:39.587 回答