Алексей Михайличенко дата публикации 10-07-2009 07:04 КАТЕГОРИЯ | | ПРОГРАММИСТ .Цикл for.Выполняется итерация для пустого списка | ПРОДУКТ | | Delphi | ПЛАТФОРМА | | Windows |
При работе со списками вроде 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, и будет вам счастье.
[Операторы, синтаксис языка.] [Целые числа]
Обсуждение материала [ 19-08-2011 00:53 ] 22 сообщения |