Delphi 2009 - ошибка? Добавление предположительно недопустимых значений в набор - PullRequest
6 голосов
/ 30 января 2011

Прежде всего, я не очень опытный программист.Я использую 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 ]

12 голосов
/ 30 января 2011

Мне было любопытно взглянуть на скомпилированный код, который создается, и я выяснил следующее о том, как наборы работают в 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 байта и устанавливается на основе перечисления, которое включает в себя только значения 0 и 101 требует (задыхается) 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;

Для тех, кто понимает Ассемблер, взгляните на сгенерированный код самостоятельно.Для тех, кто не понимает ассемблер, сгенерированный код эквивалентен:

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 проверьте с 1 и 2.Для теста set of 7..8 только с 7 и 8.Я не считаю set сломанным.Он отлично справляется со своей задачей во всем VCL (и в моем собственном коде это тоже есть).
  • По моему мнению, компилятор генерирует неоптимальный код для назначений множеств.Я не думаю, что поиск таблиц необходим, компилятор может генерировать значения в строке, и код будет иметь тот же размер, но лучшую локальность.
  • Мое мнение таково, что побочный эффект от set of 1..2 ведут себя так же, как set 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;
2 голосов
/ 30 января 2011

У меня нет «внутреннего знания», но логика компилятора кажется довольно прозрачной.

Во-первых, компилятор считает, что любой набор, такой как set of 1..2, является подмножеством set of 0..255. Вот почему set of 256..257 не допускается.

Во-вторых, компилятор оптимизирует распределение памяти - поэтому он выделяет только 1 байт для set of 1..2. Один и тот же 1 байт выделен для set of 0..7, и, похоже, нет разницы между обоими наборами на двоичном уровне. Короче говоря, компилятор выделяет как можно меньше памяти с учетом выравнивания (это означает, например, что компилятор никогда не выделяет 3 байта для set - он выделяет 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;
2 голосов
/ 30 января 2011

Согласно официальной документации на комплектах (мой акцент):

Синтаксис для конструктора множеств: [ item1, ..., itemn] где каждый элемент либо выражение, обозначающее порядковый номер базового типа

Теперь, согласно Типы поддиапазонов :

Когда вы используете цифру или символ константы для определения поддиапазона, базовый тип - наименьшее целое число или тип символа, который содержит указанный диапазон.

Поэтому, если вы укажете

type
  TNum = 1..2;

тогда базовый тип будет байтовым (скорее всего), и поэтому, если

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

тогда

test := [255];

будет работать, но не

test := [256];

все согласно официальной спецификации.

1 голос
/ 30 января 2011

Насколько я понимаю, никаких ошибок там нет.

Например, возьмите следующий код

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;  

Это, по сути, объявляет набор размеров в байтах (если вы вызываете 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;

Теперь я должен признать, что я ожидал, что "выражение константы нарушает границы поддиапазона" в первой строке (но не во 2-й)

Так что да ... может быть небольшая проблема с компилятором.

Что касается непоследовательности вашего результата ... Я почти уверен, что использование заданных значений из поддиапазонных значений не гарантирует согласованного результата в разных версиях Delphi (возможно, даже не в разных компиляциях ...) если ваш диапазон составляет 1..2, придерживайтесь [1] и [2].

1 голос
/ 30 января 2011

Вдобавок ко всему, это был побочный эффект, который позволял использовать несмежные типы перечисления.

То же самое верно для битовых флагов .NET: поскольку в обоих случаях базовые типы совместимы с целым числом, в него можно вставить любое целое число (в Delphi ограничено 0..255).

- Йерун

1 голос
/ 30 января 2011

Набор хранится как число и может фактически содержать значения, которых нет в перечислении, на котором основан набор.Я ожидал бы ошибку, по крайней мере, когда Range Checking включен в опциях компилятора, но это не так.Я не уверен, если это ошибка или дизайн.

[править]

Это странно, хотя:

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

var
  test: TSet;
  test2: TNum;

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