Цикл for.Выполняется итерация для пустого списка

Источник: delphikingdom
Алексей Михайличенко

При работе со списками вроде TList, и вообще везде, где есть свойство Count, обычным является перебор элементов в цикле for:

 list := TList.Create;

 for i := 0 to list.Count-1 do
    обращение к list.Items[i]...

Если переменная i имеет тип Integer, то все работает замечательно.

Но если переменная i имеет тип Word (напомним, этот тип допускает диапазон 0..65535, что, казалось бы, достаточно для большинства списков), то проект компилируется без проблем, но в работе происходит ошибка:

На пустом списке происходит вход в тело цикла, обращение к Items[0], и, соответственно, ошибка List index out of bounds (0).

Отключение оптимизации на ситуацию не влияет.

Понятно, что для пустого списка (Count = 0) цикл выполняться вообще не должен: for i := 0 to -1 - нет проходов.

Причина ошибки в том, что при вхождении в цикл выполняется сравнение переменной цикла типа Word (которая, как мы уже сказали, понимает только 0..65535) с числом со знаком (-1), и происходит ошибка переполнения типа Word.

Кстати, если попытаться задать границы цикла константами, то код вообще не откомпилируется, и будет выдана вполне внятная ошибка:

var i: Word;
for i := 0 to -1 do

[Error] Unit1.pas(34): Constant expression violates subrange bounds

Психологическая подоплека ошибки - в том, что при представлении заполненного списка программист совершенно справедливо подразумевает неотрицательное множество индексов - 0,1,2 и далее, и ему так и просится беззнаковая переменная цикла.

Технические подробности ошибки: код для воспроизведения, и соответствующий компилированный код:

   procedure TForm1.Button1Click(Sender: TObject);
   var
     list: TList;
     i: word;
   begin
     list := TList.Create;

34    for i := 0 to list.Count-1 do
35      if assigned(list.Items[i]) then // произвольное обращение к элементу
36        ShowMessage('assigned');
37
38    list.Free;
   end;
Unit1.pas.34: for i := 0 to list.Count-1 do

00452985 668B5F08         mov bx,[edi+$08]
00452989 4B               dec ebx
0045298A 6685DB           test bx,bx           <<<<<<<<<<<<<<<<<<<
ошибка сравнения здесь
0045298D 7221             jb +$21              <<<<<<<<<<<<<<<<<<<
перехода на конец цикла не происходит
0045298F 43               inc ebx
00452990 33F6             xor esi,esi

Unit1.pas.35: if assigned(list.Items[i]) then

00452992 0FB7D6           movzx edx,si
00452995 8BC7             mov eax,edi
00452997 E8DC15FCFF       call TList.Get
0045299C 85C0             test eax,eax
0045299E 740A             jz +$0a

Unit1.pas.36: ShowMessage('assigned');

004529A0 B8C4294500       mov eax,$004529c4
004529A5 E87E50FDFF       call ShowMessage
004529AA 46               inc esi

Unit1.pas.34: for i := 0 to list.Count-1 do

004529AB 66FFCB           dec bx
004529AE 75E2             jnz -$1e

Unit1.pas.38: list.Free;

А вот работающий код, для переменной типа Integer:

Unit1.pas.34: for i := 0 to list.Count-1 do

00452985 8B5F08           mov ebx,[edi+$08]
00452988 4B               dec ebx
00452989 85DB             test ebx,ebx
0045298B 7C1E             jl +$1e             <<<<<<<<<<<<<<<<<<<<<<<

переход происходит
0045298D 43               inc ebx
0045298E 33F6             xor esi,esi


004529A8 4B               dec ebx
004529A9 75E5             jnz -$1b




Типовые решения


Следует использовать для переборов списков только Integer-переменные цикла. А уж если важна оптимизация нескольких байт - то более короткие, но обязательно знаковые типы: Shortint -128..127, Smallint -32768..32767. Либо можно использовать границы цикла for i := 1 to Count, а обращаться к элементу Item[i-1], хотя это будет отличаться от общепринятого подхода, и может вызвать трудности у коллег (все привыкли, что текущий элемент в цикле - это i).

Также, ошибка отлавливается при установленной опции компилятора Range Checking. Еще раз повторим рекомендацию устанавливать дополнительные проверочные опции при разработке и отладке проекта.

Комментарий


Это естественное поведение компилятора, и оно характерно для всех версий Паскаля. Программист не предусмотрел возможного переполнения и получил неожиданный эффект. Обойти ошибку, оставаясь с беззнаковой переменной цикла, можно, если заранее проверять список на пустоту. Но это некрасивое решение. Наиболее грамотным решением будет использовать знаковые целые типы для переменной цикла. И не имеет смысла экономить на размере переменной цикла, даже если для итераций хватит двух или одного байта. Базовым размером для процессора является 4 байта, и с ним он работает наиболее эффективно, а компилятор генерит наиболее эффективный код.

Короче говоря, универсальным правилом является: для переменной цикла for используйте тип Integer, и будет вам счастье.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=21415