Как исправить ошибку «Индекс находится за границами диапазона» при динамическом создании колонок в таблице значений?

Программист 1С v8.3 (Обычные формы) IT и автоматизация бизнеса
← На главную

При разработке сложных отчетов или обработок в среде 1С программисты часто сталкиваются с задачей транспонирования данных — когда строки из базы данных нужно представить в виде колонок таблицы. Подобные задачи нередко решаются ручным созданием колонок в объекте ТаблицаЗначений и последующим заполнением строк в цикле. Однако при таком подходе легко совершить логическую ошибку, которая приводит к аварийному завершению программы с ошибкой «Индекс находится за границами диапазона» (или «Index value exceeds range limits»).

В этой статье мы подробно разберем, почему возникает данная ошибка в алгоритмах динамического построения таблиц, как исправить исходный код и какие альтернативные, более современные и производительные инструменты предлагает платформа 1С для решения этой задачи.

---

Выясняем причину: почему падает исходный алгоритм?

Проанализируем фрагмент кода, представленный автором темы. В нем выполняется запрос к регистру сведений, результаты выгружаются в таблицу значений ЗапросЗ, а затем создается новая пустая таблица ТабЗн, куда динамически добавляются колонки.

Ошибка происходит на строке:

ТекСтр.Установить(ТекКол, ТабСтрока.Сумма);

Это случается по нескольким причинам:

  1. Некорректная начальная индексация. В платформе 1С колонки любой таблицы значений нумеруются строго с 0. Автор инициализирует переменную ТекКол = 2. Если к моменту обращения в таблице создано менее трех колонок (индексы 0, 1 и 2), то попытка записать значение по индексу 2 вызовет исключение.
  2. Неконтролируемый инкремент индекса. Переменная ТекКол увеличивается на единицу при каждом шаге заполнения данных (ТекКол = ТекКол + 1) внутри вложенного цикла. При этом новые колонки в саму таблицу ТабЗн добавляются только при выполнении определенного условия (ТабСтрока.Операция = ТекОпер и Контроль=1). В результате значение индекса ТекКол растет лавинообразно и очень быстро превышает реальное количество колонок в таблице значений.

Использование метода Установить() по числовому индексу при динамическом формировании структуры таблицы — крайне ненадежный путь. Малейший сбой в логике циклов или изменении состава исходных данных гарантированно приведет к падению системы.

---

Решение №1. Обращение к колонкам по их именам (уникальным идентификаторам)

Чтобы раз и навсегда избавиться от проблем с индексами, откажемся от числовой адресации колонок. Вместо этого будем присваивать каждой динамически создаваемой колонке уникальное имя (строковый идентификатор) и обращаться к ней через квадратные скобки СтрокаТаблицы[ИмяКолонки].

Рассмотрим по шагам, как переписать этот алгоритм правильно и безопасно:


// Шаг 1. Создаем пустую таблицу значений и добавляем базовую колонку для аналитики
ТабЗн = Новый ТаблицаЗначений;
ТабЗн.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));

// Шаг 2. Заранее подготовим уникальные имена колонок на основе видов операций
// Чтобы избежать дублирования колонок, сначала соберем их уникальный список
СписокОпераций = Новый СписокЗначений;
Для Каждого СтрокаЗапроса Из ЗапросЗ Цикл
    Если СписокОпераций.НайтиПоЗначению(СтрокаЗапроса.Операция) = Неопределено Тогда
        СписокОпераций.Добавить(СтрокаЗапроса.Операция);
    КонецЕсли;
КонецЦикла;

// Шаг 3. Динамически добавляем колонки в таблицу значений, задавая им уникальные имена
// Имя колонки должно соответствовать правилам идентификаторов 1С (без пробелов и спецсимволов)
Для Каждого ЭлементСписка Из СписокОпераций Цикл
    ИмяОперации = ЭлементСписка.Значение;
    // Формируем безопасное имя колонки
    ИмяКолонкиКоличество = "Количество_" + СтрЗаменить(ИмяОперации, " ", "_");
    ИмяКолонкиЦена       = "Цена_" + СтрЗаменить(ИмяОперации, " ", "_");
    ИмяКолонкиСумма      = "Сумма_" + СтрЗаменить(ИмяОперации, " ", "_");
    
    // Добавляем колонки с указанием типов данных
    ТабЗн.Колонки.Добавить(ИмяКолонкиКоличество, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
    ТабЗн.Колонки.Добавить(ИмяКолонкиЦена, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
    ТабЗн.Колонки.Добавить(ИмяКолонкиСумма, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
КонецЦикла;

// Шаг 4. Заполняем таблицу данными
Для Каждого СтрокаДанных Из ЗапросЗ Цикл
    
    // Ищем, создавали ли мы уже строку для этой номенклатуры
    ПараметрыОтбора = Новый Структура("Номенклатура", СтрокаДанных.Номенклатура);
    НайденныеСтроки = ТабЗн.НайтиСтроки(ПараметрыОтбора);
    
    Если НайденныеСтроки.Количество() > 0 Тогда
        ТекущаяСтрока = НайденныеСтроки[0];
    Иначе
        ТекущаяСтрока = ТабЗн.Добавить();
        ТекущаяСтрока.Номенклатура = СтрокаДанных.Номенклатура;
    КонецЕсли;
    
    // Формируем имена колонок для текущей операции строки
    ИмяОперации = СтрокаДанных.Операция;
    ИмяКолонкиКоличество = "Количество_" + СтрЗаменить(ИмяОперации, " ", "_");
    ИмяКолонкиЦена       = "Цена_" + СтрЗаменить(ИмяОперации, " ", "_");
    ИмяКолонкиСумма      = "Сумма_" + СтрЗаменить(ИмяОперации, " ", "_");
    
    // Заполняем данные напрямую по имени колонки
    ТекущаяСтрока[ИмяКолонкиКоличество] = СтрокаДанных.Количество;
    ТекущаяСтрока[ИмяКолонкиЦена]       = СтрокаДанных.Цена;
    ТекущаяСтрока[ИмяКолонкиСумма]      = СтрокаДанных.Сумма;
    
КонецЦикла;

Этот код работает стабильно, не зависит от порядка обхода коллекций и полностью защищен от ошибок выхода индекса за допустимые пределы.

---

Решение №2. Перенос логики на сторону СУБД (Динамический PIVOT-запрос)

Выгрузка сырых данных в память сервера приложений и последующий перебор строк во вложенных циклах — ресурсоемкая операция, которая будет сильно тормозить при увеличении объема данных. Намного эффективнее переложить задачу группировки и распределения данных по колонкам на СУБД, построив динамический текст запроса.

Проанализируем, как это реализовать программно:


// Шаг 1. Получаем список уникальных операций из базы данных
ЗапросОпераций = Новый Запрос;
ЗапросОпераций.Текст = 
    "ВЫБРАТЬ РАЗЛИЧНЫЕ
    |	ИзмТЧЗак.Операция КАК Операция
    |ИЗ
    |	РегистрСведений.ИзмТЧЗак КАК ИзмТЧЗак";
ТаблицаОпераций = ЗапросОпераций.Выполнить().Выгрузить();

// Шаг 2. Динамически конструируем текст основного запроса
ТекстПолейВыбора = "";
ПараметрыЗапроса = Новый Структура;

Для Итератор = 0 По ТаблицаОпераций.Количество() - 1 Цикл
    ТекущаяОперация = ТаблицаОпераций[Итератор].Операция;
    ИмяПоля = СтрЗаменить(ТекущаяОперация, " ", "_");
    ИмяПараметра = "ПараметрОпер_" + Итератор;
    
    // Добавляем параметры для безопасного сравнения в запросе
    ПараметрыЗапроса.Вставить(ИмяПараметра, ТекущаяОперация);
    
    // Формируем динамические колонки с агрегатными функциями
    ТекстПолейВыбора = ТекстПолейВыбора + 
        ", СУММА(ВЫБОР КОГДА ИзмТЧЗак.Операция = &" + ИмяПараметра + " ТОГДА ИзмТЧЗак.Количество ИНАЧЕ 0 КОНЕЦ) КАК Количество_" + ИмяПоля + 
        ", СУММА(ВЫБОР КОГДА ИзмТЧЗак.Операция = &" + ИмяПараметра + " ТОГДА ИзмТЧЗак.Цена ИНАЧЕ 0 КОНЕЦ) КАК Цена_" + ИмяПоля + 
        ", СУММА(ВЫБОР КОГДА ИзмТЧЗак.Операция = &" + ИмяПараметра + " ТОГДА ИзмТЧЗак.Сумма ИНАЧЕ 0 КОНЕЦ) КАК Сумма_" + ИмяПоля;
КонецЦикла;

// Шаг 3. Собираем итоговый текст запроса с группировкой по номенклатуре
ЗапросДанных = Новый Запрос;
ЗапросДанных.Текст = 
    "ВЫБРАТЬ
    |	ИзмТЧЗак.Номенклатура КАК Номенклатура" + ТекстПолейВыбора + "
    |ИЗ
    |	РегистрСведений.ИзмТЧЗак КАК ИзмТЧЗак
    |ГДЕ
    |	ИзмТЧЗак.Заказ = &ТекЗаказ
    |ГРУППИРОВАТЬ ПО
    |	ИзмТЧЗак.Номенклатура";

// Устанавливаем параметры
ЗапросДанных.УстановитьПараметр("ТекЗаказ", ТекЗаказ);
Для Каждого КлючИЗначение Из ПараметрыЗапроса Цикл
    ЗапросДанных.УстановитьПараметр(КлючИЗначение.Ключ, КлючИЗначение.Значение);
КонецЦикла;

// Выгружаем готовую свернутую таблицу со всеми нужными колонками
ИтоговаяТаблица = ЗапросДанных.Выполнить().Выгрузить();

Преимущества данного метода: СУБД выполняет группировку и расчеты колонок на низком уровне чрезвычайно быстро. Программисту не нужно вручную создавать строки, искать дубли номенклатуры и заботиться об индексах колонок — метод Выгрузить() сразу вернет готовую к отображению или дальнейшему анализу таблицу значений.

---

Решение №3. Использование Системы компоновки данных (СКД)

Если конечной целью построения такой таблицы является вывод информации пользователю на экран или печать, то писать программный код для трансформации таблиц вообще не нужно. В 1С для этих целей спроектирован декларативный инструмент — Кросс-таблица СКД.

Рассмотрим, как быстро настроить кросс-таблицу в схеме компоновки данных:

  1. Создаем новый отчет и добавляем набор данных - запрос (самый простой линейный запрос без ручной группировки по операциям).
  2. Переходим на вкладку «Настройки».
  3. В структуру отчета вместо обычной детальной группировки добавляем элемент «Таблица».
  4. В поле «Строки» добавляем аналитику (например, Номенклатура).
  5. В поле «Колонки» перетаскиваем поле Операция.
  6. Вкладку «Ресурсы» заполняем показателями, которые должны рассчитываться на пересечениях строк и колонок (Количество, Цена, Сумма) с функциями СУММА() или СРЕДНЕЕ().

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

Если же динамическая таблица значений нужна строго внутри программного кода для дальнейших алгоритмических расчетов, СКД также можно запустить программно и выгрузить результат кросс-таблицы в коллекцию значений с помощью ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений.

---

Нюансы отображения динамических таблиц на Управляемых формах

В коде автора фигурирует конструкция ЭлементыФормы.Список.ТекущаяСтрока, что указывает на использование старого типа интерфейса — обычных форм. В обычных формах динамически подготовленную таблицу значений было достаточно привязать к элементу управления типа «Табличное поле» и вызвать метод СоздатьКолонки().

Если вы переносите этот функционал на современные Управляемые формы, важно помнить о ключевых отличиях архитектуры:

Понимание этих нюансов позволит вам писать надежный код, который легко масштабируется и не вызывает досадных ошибок переполнения диапазонов индексов.

← На главную