Delphi 2009 - Bug?将所谓的无效值添加到集合中

时间:2011-01-29 22:03:44

标签: delphi set delphi-2009

首先,我不是一个非常有经验的程序员。我正在使用Delphi 2009并且一直在处理集合,这些集合似乎表现得非常奇怪甚至不一致。我想这可能是我,但以下看起来显然有些不对劲:

unit test;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

type
TForm1 = class(TForm)
  Button1: TButton;
  Edit1: TEdit;
  procedure Button1Click(Sender: TObject);
private
    test: set of 1..2;
end;

var Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  test := [3];
  if 3 in test then
    Edit1.Text := '3';
end;

end.

如果您运行该程序并单击该按钮,那么,当然,它将在文本字段中显示字符串“3”。但是,如果您使用100之类的数字尝试相同的事情,则不会显示任何内容(在我看来应该如此)。我错过了什么或者这是某种错误吗?建议将不胜感激!

编辑:到目前为止,我的观察似乎并不孤单。如果有人对此有一些了解,我会很高兴听到这个消息。此外,如果有人使用Delphi 2010(甚至是Delphi XE),如果你能对这个甚至是一般设置行为(例如“test:set of 256..257”)进行一些测试,我将不胜感激。有趣的是看看在新版本中是否有任何变化。

6 个答案:

答案 0 :(得分:12)

我很好奇地看看生成的已编译代码,并且我在Delphi 2010中找到了关于集合如何工作的以下内容。它解释了为什么在test := [8]时可以test: set of 1..2进行操作,以及Assert(8 in test)之后立即失败的原因。

实际使用了多少空间?

set of byte对于每个可能的字节值都有一位,总共256位,32字节。 set of 1..2需要1个字节,但令人惊讶的是set of 100..101也需要一个字节,因此Delphi的编译器对内存分配非常聪明。在另一方面,set of 7..8需要2个字节,并且基于仅包含值0101的枚举设置需要(喘气)13个字节!

测试代码:

TTestEnumeration = (te0=0, te101=101);
TTestEnumeration2 = (tex58=58, tex101=101);

procedure Test;
var A: set of 1..2;
    B: set of 7..8;
    C: set of 100..101;
    D: set of TTestEnumeration;
    E: set of TTestEnumeration2;
begin
  ShowMessage(IntToStr(SizeOf(A))); // => 1
  ShowMessage(IntToStr(SizeOf(B))); // => 2
  ShowMessage(IntToStr(SizeOf(C))); // => 1
  ShowMessage(IntToStr(SizeOf(D))); // => 13
  ShowMessage(IntToStr(SizeOf(E))); // => 6
end;

结论:

  • 集合背后的基本模型是set of byte,有256个可能的位,32个字节。
  • Delphi确定总32字节范围所需的连续子范围并使用它。对于set of 1..2的情况,它可能只使用第一个字节,因此SizeOf()返回1.对于set of 100.101,它可能只使用第13个字节,因此SizeOf()返回1. set of 7..8它可能使用前两个字节,因此我们得到SizeOf()=2。这是一个特别有趣的情况,因为它向我们显示位不向左或向右移位以优化存储。另一个有趣的案例是set of TTestEnumeration2:它使用6个字节,即使那些周围有很多不可用的位。

编译器生成什么样的代码?

测试1,两组,均使用“第一个字节”。

procedure Test;
var A: set of 1..2;
    B: set of 2..3;
begin
  A := [1];
  B := [1];
end;

对于那些了解Assembler的人,请亲自查看生成的代码。对于那些不了解汇编程序的人,生成的代码相当于:

begin
  A := CompilerGeneratedArray[1];
  B := CompilerGeneratedArray[1];
end;

这不是拼写错误,编译器对两个赋值都使用相同的预编译值。 CompiledGeneratedArray[1] = 2

这是另一项测试:

procedure Test2;
var A: set of 1..2;
    B: set of 100..101;
begin
  A := [1];
  B := [1];
end;

同样,在伪代码中,编译后的代码如下所示:

begin
  A := CompilerGeneratedArray1[1];
  B := CompilerGeneratedArray2[1];
end;

同样,没有拼写错误:这次编译器为两个赋值使用不同的预编译值。 CompilerGeneratedArray1[1]=2 CompilerGeneratedArray2[1]=0;编译器生成的代码足够智能,不会用无效值覆盖“B”中的位(因为B保存有关位96..103的信息),但它对两个赋值使用非常相似的代码。

结论

  • 如果使用基本集中的值进行测试,则所有设置操作都能很好地工作。对于set of 1..2,请使用12进行测试。仅适用set of 7..8 78的{​​{1}}测试。我不认为set会被打破。它在整个VCL中都很有用(并且它在我自己的代码中也占有一席之地)。
  • 在我看来,编译器会为集合分配生成次优代码。我不认为表查找是必需的,编译器可以生成内联值,代码将具有相同的大小但更好的位置。
  • 我的观点是,set of 1..2set of 0..7的行为相同的副作用是编译器先前缺乏优化的副作用。
  • 在OP的情况下(var test: set of 1..2; test := [7]),编译器应该生成错误。我不会将其归类为错误,因为我不认为编译器的行为应该根据“程序员对错误代码做什么”来定义,而是根据“程序员如何处理好代码” “;尽管如此,编译器应该生成Constant expression violates subrange bounds,如果你尝试这个代码那样:

(代码示例)

procedure Test;
var t: 1..2;
begin
  t := 3;
end;
  • 在运行时,如果使用{$R+}编译代码,则错误的分配应该引发错误,就像尝试此代码时一样:

(代码示例)

procedure Test;
var t: 1..2;
    i: Integer;
begin
  {$R+}
  for i:=1 to 3 do
    t := i;
  {$R-}
end;

答案 1 :(得分:2)

根据官方文件on sets(我的重点):

  

集合构造函数的语法是:[   item1,...,itemn]每个项目的位置   表达一个表达式   集合的基本类型

的序数

现在,根据Subrange types

  

使用数字或字符时   用于定义子范围的常量,   base类型是最小的整数或   包含的字符类型   指定范围。

因此,如果您指定

type
  TNum = 1..2;

然后基类型将是字节(最有可能),因此,如果

type
  TSet = set of TNum;
var
  test: TSet;

然后

test := [255];

可以使用,但不是

test := [256];

全部根据官方规范。

答案 2 :(得分:2)

我没有“内部知识”,但编译逻辑似乎相当透明。

首先,编译器认为像set of 1..2这样的任何集都是set of 0..255的子集。这就是为什么set of 256..257不被允许的原因。

其次,编译器优化了内存分配 - 因此它只为set of 1..2分配1个字节。为set of 0..7分配了相同的1个字节,并且在二进制级别上两个集之间似乎没有差异。简而言之,编译器在考虑对齐的情况下分配尽可能少的内存(这意味着例如编译器永远不会为set分配3个字节 - 它分配4个字节,即使set适合3个字节,如set of 1..20)。

编译器处理sets的方式存在一些不一致,可以通过以下代码示例演示:

type
   TTestSet = set of 1..2;
   TTestRec = packed record
     FSet: TTestSet;
     FByte: Byte;
   end;

var
  Rec: TTestRec;

procedure TForm9.Button3Click(Sender: TObject);
begin
  Rec.FSet:= [];
  Rec.FByte:= 1;           // as a side effect we set 8-th element of FSet
                           //   (FSet actually has no 8-th element - only 0..7)
  Assert(8 in Rec.FSet);   // The assert should fail, but it does not!
  if 8 in Rec.FSet then    // another display of the bug
    Edit1.Text := '8';
end;

答案 3 :(得分:1)

一个集合存储为一个数字,实际上可以保存不在该集合所基于的枚举中的值。我希望有一个错误,至少在编译器选项中启用了Range Checking时,但似乎并非如此。我不确定这是一个错误还是设计。

[编辑]

但奇怪的是:

type
  TNum = 1..2;
  TSet = set of TNum;

var
  test: TSet;
  test2: TNum;

test2 := 4;  // Not accepted
test := [4]; // Accepted

答案 4 :(得分:1)

从我的头脑中,这是允许非连续枚举类型的副作用。

.NET bitflags也是如此:因为在这两种情况下,底层类型都与整数兼容,你可以在其中插入任何整数(在Delphi中限制为0..255)。

- 的Jeroen

答案 5 :(得分:1)

就我而言,没有错误。

例如,请使用以下代码

var aByte: Byte;
begin
  aByte := 255;
  aByte := aByte + 1;
  if aByte = 0 then
    ShowMessage('Is this a bug?');
end;

现在,您可以从此代码中获得2个结果。如果使用Range Checking TRUE进行编译,则会在第2行引发异常。如果您没有使用范围检查进行编译,则代码将在没有任何错误的情况下执行并显示消息对话框。

你遇到的情况类似,除了没有编译器开关强制在这种情况下引发异常(嗯,据我所知......)。

现在,从您的例子开始:

private         
  test: set of 1..2;  

基本上声明了一个Byte大小的集合(如果你调用SizeOf(Test),它应该返回1)。字节大小的集合只能包含8个元素。在这种情况下,它可以包含[0]到[7]。

现在,有些例子:

begin
  test := [8]; //Here, we try to set the 9th bit of a Byte sized variable. It doesn't work
  Test := [4]; //Here, we try to set the 5th bit of a Byte Sized variable. It works.      
end;

现在,我需要承认我会期望第一行的“常量表达式违反子范围界限”(但不是第二行)

所以是的......编译器可能存在一个小问题。

至于你的结果是不一致的...我很确定使用set的子范围值中的set值不能保证在不同版本的Delphi上给出一致的结果(也许甚至不是在不同的编译中......所以如果您的范围是1..2,请坚持[1]和[2]。