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