При обработке табличных частей документов начинающие разработчики нередко сталкиваются с исключением платформы: «Значение индекса выходит за границы диапазона». Разберем по шагам, почему возникает эта ошибка, как правильно адресовать строки в цикле и каким образом можно оптимизировать работу с данными, чтобы не перегружать информационную базу.
Проанализируем исходный фрагмент кода, вызвавший сбой в работе программы:
Чек = Документы.ЧекККМ;
Ч = Чек.Выбрать(НачалоДня(ДатаЧеков), КонецДня(ДатаЧеков));
Пока Ч.Следующий() Цикл
Для Каждого ТекущаяСтрока Из Ч.Товары Цикл
Ном = ТекущаяСтрока.НомерСтроки;
кодТ = Прав(Ч.Товары.Получить(Ном).Номенклатура.Код, 5);
// ...
Выясним причину ошибки. В платформе 1С:Предприятие коллекция строк табличной части, как и любой массив или список, использует индексацию с нуля. Первый элемент находится по индексу 0, а последний — по индексу Количество() - 1.
В то же время свойство НомерСтроки строки табличной части предназначено для отображения порядкового номера пользователю в интерфейсе, поэтому нумерация там всегда начинается с единицы. Когда цикл доходит до последней строки табличной части (например, строка номер 10 при общем количестве 10), вызов Ч.Товары.Получить(10) пытается обратиться к одиннадцатому элементу несуществующего индекса. В этот момент платформа аварийно завершает выполнение с ошибкой выхода за границы диапазона.
Посмотрим на конструкцию цикла Для Каждого ... Из ... Цикл. В данном цикле переменная ТекущаяСтрока на каждой итерации уже содержит прямую ссылку на текущую обрабатываемую строку табличной части. Повторное обращение к коллекции через метод Получить() является абсолютно избыточным.
Упростим и исправим код, обратившись к номенклатуре напрямую через текущую строку:
Пока Ч.Следующий() Цикл
Для Каждого ТекущаяСтрока Из Ч.Товары Цикл
кодТ = Прав(ТекущаяСтрока.Номенклатура.Код, 5);
// дальнейшая обработка
КонецЦикла;
КонецЦикла;
Такой вариант избавляет от необходимости высчитывать индекс строки, делает код лаконичным, читаемым и исключает ошибку индексации.
Если по каким-либо причинам требуется обращаться к строке именно по ее номеру, необходимо учитывать смещение индекса на единицу:
Ном = ТекущаяСтрока.НомерСтроки;
кодТ = Прав(Ч.Товары.Получить(Ном - 1).Номенклатура.Код, 5);
Этот способ устраняет исключение, однако он считается неоптимальным стилем программирования в 1С при наличии итератора ТекущаяСтрока.
Рассмотрим важные архитектурные нюансы представленного алгоритма. Использование объекта ДокументВыборка (метод Выбрать()) и обращение через точку к табличной части Ч.Товары приводит к тому, что платформа неявно считывает каждый документ из базы данных целиком. Разыменование ссылок вроде ТекущаяСтрока.Номенклатура.Код в цикле порождает множество мелких запросов к СУБД, что критически замедляет работу системы при обработке большого объема документов за период.
Кроме того, выборка предназначена только для чтения. Если необходимо не просто получить код, но и записать новые значения в строку документа, потребуется получать объект документа через Ч.ПолучитьОбъект() и вызывать метод Записать().
Для быстрого чтения данных и соединения с дополнительными источниками (например, если требуется получить реквизиты, хранящиеся в регистре сведений) наилучшей практикой является использование запросов:
Документ.ЧекККМ.Товары.СрезПоследних для периодических регистров) по полю Номенклатура.ПОДСТРОКА прямо в тексте запроса или обработать результат функцией Прав() при обходе выборки результата запроса.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЧекТовары.Ссылка КАК СсылкаНаДокумент,
| ЧекТовары.НомерСтроки КАК НомерСтроки,
| ЧекТовары.Номенклатура.Код КАК ПолныйКод,
| Цены.Цена КАК ЦенаИзРегистра
|ИЗ
| Документ.ЧекККМ.Товары КАК ЧекТовары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК Цены
| ПО ЧекТовары.Номенклатура = Цены.Номенклатура
|ГДЕ
| ЧекТовары.Ссылка.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания";
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаЧеков));
Запрос.УстановитьПараметр("ДатаОкончания", КонецДня(ДатаЧеков));
Запрос.УстановитьПараметр("Период", КонецДня(ДатаЧеков));
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
кодТ = Прав(Выборка.ПолныйКод, 5);
// Обработка данных
КонецЦикла;
Применение языка запросов кардинально ускоряет обработку данных и предотвращает типовые ошибки работы с коллекциями объектов в оперативной памяти.