我刚刚发现我必须重新实现的软件广泛使用 System.Round()。问题是这个函数使用“银行家四舍五入”并且行为不能像 Math.RoundTo() (rmDown,rmUp,rmNearest,rmTruncate) 那样改变。
我必须将行为更改为“正常舍入”(12.5 -> 13 NOT 12.5 -> 12)......所以我想全局覆盖 System.Round() 。我想这样做,因为 Round() 被使用了很多次,我不想手动更改它们。
这怎么可能?
我刚刚发现我必须重新实现的软件广泛使用 System.Round()。问题是这个函数使用“银行家四舍五入”并且行为不能像 Math.RoundTo() (rmDown,rmUp,rmNearest,rmTruncate) 那样改变。
我必须将行为更改为“正常舍入”(12.5 -> 13 NOT 12.5 -> 12)......所以我想全局覆盖 System.Round() 。我想这样做,因为 Round() 被使用了很多次,我不想手动更改它们。
这怎么可能?
警告:虽然下面的答案解决了所提出的问题,但我建议没有人使用它。如果您想以不同的方式执行舍入Round
,请编写并调用专用函数。
您可以使用运行时代码挂钩来更改Round
.
Round
皱纹是,虽然它是一个内在函数,但获取函数的地址有点棘手。您还必须小心遵循使用的调用约定。输入值在 x87 堆栈寄存器中传递,ST(0)
返回值是 64 位整数EDX:EAX
。
这是如何做到的。
procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
OldProtect: DWORD;
begin
if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
begin
Move(NewCode, Address^, Size);
FlushInstructionCache(GetCurrentProcess, Address, Size);
VirtualProtect(Address, Size, OldProtect, @OldProtect);
end;
end;
type
PInstruction = ^TInstruction;
TInstruction = packed record
Opcode: Byte;
Offset: Integer;
end;
procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
NewCode: TInstruction;
begin
NewCode.Opcode := $E9;//jump relative
NewCode.Offset :=
NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;
function System_Round: Pointer;
asm
MOV EAX, offset System.@Round
end;
procedure _ROUND;
asm
{ -> FST(0) Extended argument }
{ <- EDX:EAX Result }
// your implementation goes here
end;
initialization
RedirectProcedure(System_Round, @_ROUND);
如果您宁愿在 Pascal 中实现您的版本而不是 asm,那么您需要使非标准调用约定适应_ROUND
标准的 Delphi 调用约定。像这样:
function MyRound(x: Extended): Int64;
begin
// your implementation goes here
end;
procedure _ROUND;
var
x: Extended;
asm
{ -> FST(0) Extended argument }
{ <- EDX:EAX Result }
FSTP TBYTE PTR [x]
CALL MyRound
end;
请注意,我在这里假设您的程序是针对 32 位的。如果您需要针对 64 位,则原理大致相同,但细节明显不同。
UNIT MathRound;
INTERFACE
FUNCTION ROUND(X : Extended) : Int64;
IMPLEMENTATION
FUNCTION ROUND(X : Extended) : Int64;
BEGIN
Result:=TRUNC(X+0.5)
END;
END.
如果您将上述内容保存在项目目录中的 MathRound.PAS 中,然后在源文件中包含此单元,您将拥有一个数学 ROUND 函数,而不是默认实现的银行家舍入。
它会将 -12.5 舍入到 -12(即,对于 0.5 值,总是向零舍入)和 -12.1 到 -11。如果你想要一个更“逻辑”的舍入,你应该改用这一行:
IF X<0.0 THEN Result:=-TRUNC(ABS(X)+0.5) ELSE Result:=TRUNC(X+0.5)
作为函数体。
这将导致
ROUND(12.5) = 13
ROUND(12.1) = 12
ROUND(-12.5)= -13
ROUND(-12.1)= -12
您担心手动将所有现有Round
呼叫更改为呼叫其他呼叫所需的时间和精力。所以不要手动更改它们。使用工具使其自动化。例如,您可以使用 sed。
sed -i -e "s/\bRound\b/BiasedRoundAwayFromZero/g" *.pas
通过该更改,您的代码现在可以明确说明它使用的舍入方式。它不需要每个阅读您的代码的人都知道在代码的其他地方应用了补丁来影响标准函数的全局行为。它也不会影响您从其他库链接到的代码,这些代码可能依赖于全局更改的标准行为Round
并被全局更改破坏。
// 银行家轮 omzeilen
function RoundN(X: double): double;
const
cFuncName = 'RoundN';
begin
Result := Trunc(X + Frac(X));
end;