范围检查错误的原因(Delphi)

12

以下是一段代码的简化版本,它会引起范围检查错误和溢出错误,我应该打开编译器检查指令吗?我知道为什么会发生溢出,在C1的乘法中,似乎很可能超过数据类型的最大值。但为什么这也会触发范围检查错误?Delphi的文档以及stackoverflow上的其他帖子都表明,范围检查错误通常是用于超出数组边界的访问。但在报告范围检查错误的那一行,我没有访问数组。也许是在对param1的赋值时?但如果是这样的话,为什么会是范围检查错误,而不是溢出错误呢?

const
  C1 = 44001;
  C2 = 17999;

function fxnName(..other params...; param1: Word): String;
var
  someByte: byte;
begin
  // some code
  // by now we're in a loop. the following line is where it breaks to in the debugger: 
  param1 := (someByte + param1) * C1 + C2;
  // more code
end;

如果相关的话,在调试器中当它在那一行出错时,所有的值看起来都是预期的,除了param1,当我要求Delphi评估它时,它显示为“未声明的标识符:'param1'”。


我猜测这仅是因为溢出检查之前进行了范围检查,一旦引发范围检查异常,溢出检查就不再发生。(对顺序不确定,这只是一种猜测。) - Ken White
2个回答

22

关于范围检查的文档:

$R指令启用或禁用范围检查代码生成。在{$R+}状态下,所有数组和字符串索引表达式都被验证为在定义的边界内,并检查所有标量和子范围变量的赋值是否在范围内。如果范围检查失败,则会引发ERangeError异常(如果未启用异常处理,则终止程序)。

因此,这里的原因是对标量值的赋值,其被分配了一个超过上限范围的值。

有关简单类型和子范围类型的范围检查错误,请参见docwiki Simple Types

示例:

{$R+} // Range check on
var
  w1,w2 : word;
begin
  w1 := High(word);
  w1 := w1 + 10; // causes range-check error on assignment to w1 (upper range passed)
  w2 := 0;
  w2 := w2 - 10; // causes range-check error on assignment to w2 (lower range passed)
end;

对于所有平台无关的整数类型,进行$R和$Q的所有组合的汇总测试:

            R+Q+  R+Q-  R-Q+
 ShortInt    R     R     x
 SmallInt    R     R     x
 Integer     O     x     O
 LongInt     O     x     O
 Int64       O     x     O
 Byte        R     R     x
 Word        R     R     x
 LongWord    O     x     O
 Cardinal    O     x     O
 UInt64      O     x     O

R=范围错误;O=溢出错误;x=无

测试使用XE2在32位模式下的伪代码:

number := High(TNumber);
number := number + 1;

1
@DavidHeffernan,请阅读文档,我刚刚测试了代码,它确实会生成范围检查错误。你必须仔细阅读逗号后面的内容,特别是指出所有对标量和子范围变量的赋值都会被检查的部分。 - LU RD
如果数据类型是整数,那么很可能会出现溢出错误。 - David Heffernan
如果计算是在“整数”类型上执行的,那么肯定可以。但是当i是一个值为MaxInt的“整数”时,“i:= i + Int64(1);”应该(未经测试)会导致范围错误。 - user743382
2
不错的表格!总结一下,输入<32字节->范围错误,输入=>32字节->溢出错误。相当奇怪的。。 - Sertac Akyuz

0
我见过太多的Delphi程序员编写相当大的程序,却从未激活范围、溢出和断言检查。当然,如果你愿意,你可以这样做,但你的代码会更加容易出错。
因此,如果你允许我插入一个类比(虽然仍然与错误有关)并回答你的问题,希望能说服更多的程序员立即启用这三个检查。最后还包括一些警告。

溢出检查

这将检查某些整数算术运算(+,-,*,Abs,Sqr,Succ,Pred,Inc和Dec)是否会发生溢出。例如,在执行加法操作后,编译器将插入额外的二进制代码来验证操作结果是否在支持的范围内。

“整数溢出”是指对整数变量进行的操作产生的结果超出了该变量的范围。例如,如果将整数变量声明为16位有符号整数,则其值的范围可以从-32768到32767。如果对此变量进行的操作产生的结果大于32767或小于-32768,则发生了整数溢出。

当发生整数溢出时,操作的结果是未定义的,并且可能导致程序中的未定义行为: • 包装 结果可能会导致包装的值。这意味着数字32768实际上将存储为1,因为它比我们可以存储的最高值(32767)高1个单位。 • 截断 结果可能会被截断或以其他方式修改,以适应整数类型的范围。例如,数字32768实际上将存储为32767,因为那是我们可以存储的最高值。

未定义的程序行为是最糟糕的错误之一,因为它不容易重现。因此,很难跟踪和修复。

如果您激活此功能,则需要付出一点代价:程序的速度会略微降低。

IO检查

检查I/O操作的结果。如果I/O操作失败,则会引发异常。如果关闭此开关,则必须手动检查I/O错误。 如果您激活此功能,则需要付出一点代价:程序的速度会降低,但由于此检查引入的几微秒与I/O操作本身所需的毫秒级时间相比微不足道(硬盘速度较慢),因此影响不大。

范围检查

Delphi Geek将其称为“最重要的Delphi设置”,我完全同意。它检查所有数组和字符串索引表达式是否在定义的范围内。它还检查所有标量和子范围变量的赋值是否在范围内。

以下是一个示例代码,如果没有范围检查,则会破坏我们的生活:

Type 
    Pasword= array [1..10] of byte; // we define an array of 10 elements
…
x:= Pasword[20];       // Range Checking will prevent the program from accessing element 20 (ERangecheckError exception is raised). Security breach avoided. Error log automatically sent to the programmer. Bruce Willis saves everyone.

启用运行时错误检查

要激活运行时错误检查,请转到项目选项并勾选以下三个框:

在“项目选项”中启用运行时错误检查 enter image description here

断言

一个好的程序员必须在其代码中使用断言来提高程序的质量和稳定性。认真点!你真的需要使用它们。

断言用于检查程序在某个特定点上应始终为真的条件,并在未满足条件时引发异常。通常使用在 SysUtils 单元中定义的 Assert 过程来执行断言。

您可以将断言视为穷人版的单元测试。我强烈建议您深入了解断言。它们非常有用,而且不需要像单元测试那样多的工作。

典型示例:

SysUtils.Assert(Input <> Nil, ‘The input should not be nil!’);

但是,为了让程序检查我们的断言,我们需要在项目设置->编译器选项中激活此功能,否则它们将被忽略,就像它们不在我们的代码中一样。确保您理解我刚才说的含义!例如,如果我们在Assert中调用具有副作用的例程,那么我们会犯严重错误。在下面的示例中,在调试时启用断言时,Test()函数将被执行,并且'This was executed'将出现在Memo中。然而,在发布模式下,该文本将不会出现在备忘录中,因为现在Assert被简单地忽略了。恭喜,我们刚刚使程序在调试/发布模式下表现不同 ☹。

function TMainForm.Test: Boolean;
begin
 Result:= FALSE;
 mmo.Lines.Add('This was executed');
end;

procedure TMainForm.Start;
VAR x: Integer;
begin
 x:= 0;
 if x= 0
 then Assert(Test(), 'nope');
end;

以下是一些使用示例:
1. 检查输入参数是否在0..100范围内:
procedure DoSomething(value: Integer);
begin
  Assert((value >= 0) and (value <= 100), 'Value out of range');
  …
end;

2 在使用指针之前,检查它是否为非空:

Var p: Pointer;
Begin
  p := GetPointer;
  Assert(Assigned(p), 'Pointer is nil');
   …
End;

在继续之前检查变量是否具有特定值的方法:

var i: Integer;
begin
   i := GetValue;
   Assert(i = 42, 'Incorrect response to “What is the answer to life”!');
  …
end;

通过定义项目选项中的NDEBUG符号或使用{$D-}编译器指令,我们也可以禁用断言。

在某些情况下,Assert也可以作为处理错误和异常的更优雅的方式,因为它更易读,并且还包括自定义消息,这有助于开发人员了解出了什么问题。

个人而言,我经常在例程顶部使用它来检查参数是否有效。

激活此功能会使您的程序变得更慢,因为…额外增加了一行代码。

没有免费的午餐

所有美好的东西都是有代价的(幸运的是,在我们的情况下只是一个小代价):启用运行时错误检查和断言会减慢我们的程序并使其稍微变大。

今天的计算机具有大量RAM,因此大小的轻微增加无关紧要,因此,让我们将其放在一边。但是让我们看看速度,因为这不是我们可以轻易忽略的事情:

Type                 Disabled   Enabled
Range checking       73ms         120ms
Overflow checking    580ms        680ms
I/O checking         Not tested   Not tested

正如我们所看到的,程序的速度受这些运行时检查的影响非常大。如果我们有一个速度至关重要的程序,在调试期间最好激活“运行时错误检查”。我通常会在第一次发布时保持其处于激活状态,并等待几周。如果没有报告错误,则发布一个关闭“运行时错误检查”的更新。

个人而言,我总是保持“IO检查”处于激活状态。由于此检查的性能损失微不足道。


重要警告:
如果您有一个已经写得不太好的现有项目,并且激活下面任何一项运行时错误检查,则您的程序可能会比通常更经常崩溃。 不,运行时错误检查例程没有破坏您的程序。它一直是有问题的-您只是不知道而已。运行时检查例程现在正在找到那些代码可疑、低劣和发臭的地方。运行时检查的唯一目的是为了找出程序中的错误。


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