Странные результаты с сопоставлением стоимости валюты/постоянного значения
При компиляции с 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. код отлично работает с другими значениями.
Этот ответ Барри Келли объясняет, что тип валюты "не подвержен ошибкам точности так же, как и код с плавающей запятой".
Ответы
Ответ 1
Так как жесткое литье, такое как 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;
другой способ мог бы заставлять использование curreny использовать константу или переменную
const
comp:Currency=1.32;
var
C: Currency;
begin
C := 1.32;
if C < comp then
begin
writeln ('strange');
end;
end;
Ответ 2
Это будет регрессия в Delphi.
Вывод "странный" в 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. Это точно представляемое двоичное значение с плавающей запятой. Битовая диаграмма для 13200, хранящаяся как 10-байтовый поплавок, составляет:
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 байтов float.
- Умножьте это значение на 10000.
- Сохраните полученный 10-байтовый поплавок в
$00405118
.
Поскольку 1.32 не является точно представимым, получается, что окончательный 10-байтовый поплавок не является точно 13200. И, предположительно, регрессия возникла, когда компилятор переключился с хранения этих литералов в 4 байтовых поплавках на хранение их в полях с байтом по 10 байтов.
Основная проблема заключается в том, что поддержка Delphi для типа данных 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');
Нет никакого способа, чтобы компилятор смог это сделать!
Гм!
Ответ 3
Чтобы избежать этой проблемы (ошибка в компиляторе), вы можете сделать, как предлагает @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
.
Ответ 4
Чтобы добавить к Дэвиду ответ - следующий код не странный, хотя он эквивалентен 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), поэтому проблема, похоже, связана с неудачной реализацией типа Currency
в компиляторе Delphi.