货币价值/常数价值比较的奇怪结果

时间:2013-01-16 12:23:31

标签: delphi compare delphi-2009

使用Delphi 2009编译并运行时,此控制台应用程序会写“奇怪”。 “小于”运算符两边的值相等,但代码的行为就好像它们不相等。我该怎么做才能避免这个问题?

program Project5;

{$APPTYPE CONSOLE}

var
  C: Currency;
begin
  C := 1.32;

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

  ReadLn;
end.

P.S。代码适用于其他值。

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

4 个答案:

答案 0 :(得分:8)

这似乎是德尔福的回归。

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字节的浮点数。

我的猜测是,面对1.32时,2010年的编译器会执行以下操作:

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

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

根本问题在于,Delphi对Currency数据类型的支持建立在一个完全有缺陷的设计上。使用二进制浮点算法实现十进制定点数据类型只是在寻找麻烦。修复设计的唯一理智方法是完全重新设计编译器以使用定点整数算法。值得注意的是,新的64位编译器使用与32位编译器相同的设计。

老实说,我会阻止Delphi编译器使用Currency文字进行任何浮点运算。这只是一个完整的雷区。我会像这样在脑海里做10000次转移:

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

然后调用代码将是:

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

编译器无法搞砸了!

哼!

答案 1 :(得分:4)

如同像货币(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;

答案 2 :(得分: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

答案 3 :(得分: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)生成了正确的二进制值,因此问题似乎与Delphi编译器中的错误Currency类型实现有关。