![]() |
Цикл 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, и будет вам счастье. |