При разработке сложных отчетов или обработок в среде 1С программисты часто сталкиваются с задачей транспонирования данных — когда строки из базы данных нужно представить в виде колонок таблицы. Подобные задачи нередко решаются ручным созданием колонок в объекте ТаблицаЗначений и последующим заполнением строк в цикле. Однако при таком подходе легко совершить логическую ошибку, которая приводит к аварийному завершению программы с ошибкой «Индекс находится за границами диапазона» (или «Index value exceeds range limits»).
В этой статье мы подробно разберем, почему возникает данная ошибка в алгоритмах динамического построения таблиц, как исправить исходный код и какие альтернативные, более современные и производительные инструменты предлагает платформа 1С для решения этой задачи.
---
Проанализируем фрагмент кода, представленный автором темы. В нем выполняется запрос к регистру сведений, результаты выгружаются в таблицу значений ЗапросЗ, а затем создается новая пустая таблица ТабЗн, куда динамически добавляются колонки.
Ошибка происходит на строке:
ТекСтр.Установить(ТекКол, ТабСтрока.Сумма);
Это случается по нескольким причинам:
ТекКол = 2. Если к моменту обращения в таблице создано менее трех колонок (индексы 0, 1 и 2), то попытка записать значение по индексу 2 вызовет исключение.ТекКол увеличивается на единицу при каждом шаге заполнения данных (ТекКол = ТекКол + 1) внутри вложенного цикла. При этом новые колонки в саму таблицу ТабЗн добавляются только при выполнении определенного условия (ТабСтрока.Операция = ТекОпер и Контроль=1). В результате значение индекса ТекКол растет лавинообразно и очень быстро превышает реальное количество колонок в таблице значений.Использование метода Установить() по числовому индексу при динамическом формировании структуры таблицы — крайне ненадежный путь. Малейший сбой в логике циклов или изменении состава исходных данных гарантированно приведет к падению системы.
---
Чтобы раз и навсегда избавиться от проблем с индексами, откажемся от числовой адресации колонок. Вместо этого будем присваивать каждой динамически создаваемой колонке уникальное имя (строковый идентификатор) и обращаться к ней через квадратные скобки СтрокаТаблицы[ИмяКолонки].
Рассмотрим по шагам, как переписать этот алгоритм правильно и безопасно:
// Шаг 1. Создаем пустую таблицу значений и добавляем базовую колонку для аналитики
ТабЗн = Новый ТаблицаЗначений;
ТабЗн.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
// Шаг 2. Заранее подготовим уникальные имена колонок на основе видов операций
// Чтобы избежать дублирования колонок, сначала соберем их уникальный список
СписокОпераций = Новый СписокЗначений;
Для Каждого СтрокаЗапроса Из ЗапросЗ Цикл
Если СписокОпераций.НайтиПоЗначению(СтрокаЗапроса.Операция) = Неопределено Тогда
СписокОпераций.Добавить(СтрокаЗапроса.Операция);
КонецЕсли;
КонецЦикла;
// Шаг 3. Динамически добавляем колонки в таблицу значений, задавая им уникальные имена
// Имя колонки должно соответствовать правилам идентификаторов 1С (без пробелов и спецсимволов)
Для Каждого ЭлементСписка Из СписокОпераций Цикл
ИмяОперации = ЭлементСписка.Значение;
// Формируем безопасное имя колонки
ИмяКолонкиКоличество = "Количество_" + СтрЗаменить(ИмяОперации, " ", "_");
ИмяКолонкиЦена = "Цена_" + СтрЗаменить(ИмяОперации, " ", "_");
ИмяКолонкиСумма = "Сумма_" + СтрЗаменить(ИмяОперации, " ", "_");
// Добавляем колонки с указанием типов данных
ТабЗн.Колонки.Добавить(ИмяКолонкиКоличество, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
ТабЗн.Колонки.Добавить(ИмяКолонкиЦена, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
ТабЗн.Колонки.Добавить(ИмяКолонкиСумма, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
КонецЦикла;
// Шаг 4. Заполняем таблицу данными
Для Каждого СтрокаДанных Из ЗапросЗ Цикл
// Ищем, создавали ли мы уже строку для этой номенклатуры
ПараметрыОтбора = Новый Структура("Номенклатура", СтрокаДанных.Номенклатура);
НайденныеСтроки = ТабЗн.НайтиСтроки(ПараметрыОтбора);
Если НайденныеСтроки.Количество() > 0 Тогда
ТекущаяСтрока = НайденныеСтроки[0];
Иначе
ТекущаяСтрока = ТабЗн.Добавить();
ТекущаяСтрока.Номенклатура = СтрокаДанных.Номенклатура;
КонецЕсли;
// Формируем имена колонок для текущей операции строки
ИмяОперации = СтрокаДанных.Операция;
ИмяКолонкиКоличество = "Количество_" + СтрЗаменить(ИмяОперации, " ", "_");
ИмяКолонкиЦена = "Цена_" + СтрЗаменить(ИмяОперации, " ", "_");
ИмяКолонкиСумма = "Сумма_" + СтрЗаменить(ИмяОперации, " ", "_");
// Заполняем данные напрямую по имени колонки
ТекущаяСтрока[ИмяКолонкиКоличество] = СтрокаДанных.Количество;
ТекущаяСтрока[ИмяКолонкиЦена] = СтрокаДанных.Цена;
ТекущаяСтрока[ИмяКолонкиСумма] = СтрокаДанных.Сумма;
КонецЦикла;
Этот код работает стабильно, не зависит от порядка обхода коллекций и полностью защищен от ошибок выхода индекса за допустимые пределы.
---
Выгрузка сырых данных в память сервера приложений и последующий перебор строк во вложенных циклах — ресурсоемкая операция, которая будет сильно тормозить при увеличении объема данных. Намного эффективнее переложить задачу группировки и распределения данных по колонкам на СУБД, построив динамический текст запроса.
Проанализируем, как это реализовать программно:
// Шаг 1. Получаем список уникальных операций из базы данных
ЗапросОпераций = Новый Запрос;
ЗапросОпераций.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| ИзмТЧЗак.Операция КАК Операция
|ИЗ
| РегистрСведений.ИзмТЧЗак КАК ИзмТЧЗак";
ТаблицаОпераций = ЗапросОпераций.Выполнить().Выгрузить();
// Шаг 2. Динамически конструируем текст основного запроса
ТекстПолейВыбора = "";
ПараметрыЗапроса = Новый Структура;
Для Итератор = 0 По ТаблицаОпераций.Количество() - 1 Цикл
ТекущаяОперация = ТаблицаОпераций[Итератор].Операция;
ИмяПоля = СтрЗаменить(ТекущаяОперация, " ", "_");
ИмяПараметра = "ПараметрОпер_" + Итератор;
// Добавляем параметры для безопасного сравнения в запросе
ПараметрыЗапроса.Вставить(ИмяПараметра, ТекущаяОперация);
// Формируем динамические колонки с агрегатными функциями
ТекстПолейВыбора = ТекстПолейВыбора +
", СУММА(ВЫБОР КОГДА ИзмТЧЗак.Операция = &" + ИмяПараметра + " ТОГДА ИзмТЧЗак.Количество ИНАЧЕ 0 КОНЕЦ) КАК Количество_" + ИмяПоля +
", СУММА(ВЫБОР КОГДА ИзмТЧЗак.Операция = &" + ИмяПараметра + " ТОГДА ИзмТЧЗак.Цена ИНАЧЕ 0 КОНЕЦ) КАК Цена_" + ИмяПоля +
", СУММА(ВЫБОР КОГДА ИзмТЧЗак.Операция = &" + ИмяПараметра + " ТОГДА ИзмТЧЗак.Сумма ИНАЧЕ 0 КОНЕЦ) КАК Сумма_" + ИмяПоля;
КонецЦикла;
// Шаг 3. Собираем итоговый текст запроса с группировкой по номенклатуре
ЗапросДанных = Новый Запрос;
ЗапросДанных.Текст =
"ВЫБРАТЬ
| ИзмТЧЗак.Номенклатура КАК Номенклатура" + ТекстПолейВыбора + "
|ИЗ
| РегистрСведений.ИзмТЧЗак КАК ИзмТЧЗак
|ГДЕ
| ИзмТЧЗак.Заказ = &ТекЗаказ
|ГРУППИРОВАТЬ ПО
| ИзмТЧЗак.Номенклатура";
// Устанавливаем параметры
ЗапросДанных.УстановитьПараметр("ТекЗаказ", ТекЗаказ);
Для Каждого КлючИЗначение Из ПараметрыЗапроса Цикл
ЗапросДанных.УстановитьПараметр(КлючИЗначение.Ключ, КлючИЗначение.Значение);
КонецЦикла;
// Выгружаем готовую свернутую таблицу со всеми нужными колонками
ИтоговаяТаблица = ЗапросДанных.Выполнить().Выгрузить();
Преимущества данного метода: СУБД выполняет группировку и расчеты колонок на низком уровне чрезвычайно быстро. Программисту не нужно вручную создавать строки, искать дубли номенклатуры и заботиться об индексах колонок — метод Выгрузить() сразу вернет готовую к отображению или дальнейшему анализу таблицу значений.
---
Если конечной целью построения такой таблицы является вывод информации пользователю на экран или печать, то писать программный код для трансформации таблиц вообще не нужно. В 1С для этих целей спроектирован декларативный инструмент — Кросс-таблица СКД.
Рассмотрим, как быстро настроить кросс-таблицу в схеме компоновки данных:
Номенклатура).Операция.Количество, Цена, Сумма) с функциями СУММА() или СРЕДНЕЕ().Платформа автоматически сгенерирует сложнейший макет кросс-таблицы, добавит необходимые колонки для каждой встретившейся операции, отсортирует их и выведет итоги. При этом производительность такого решения будет максимально возможной.
Если же динамическая таблица значений нужна строго внутри программного кода для дальнейших алгоритмических расчетов, СКД также можно запустить программно и выгрузить результат кросс-таблицы в коллекцию значений с помощью ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений.
---
В коде автора фигурирует конструкция ЭлементыФормы.Список.ТекущаяСтрока, что указывает на использование старого типа интерфейса — обычных форм. В обычных формах динамически подготовленную таблицу значений было достаточно привязать к элементу управления типа «Табличное поле» и вызвать метод СоздатьКолонки().
Если вы переносите этот функционал на современные Управляемые формы, важно помнить о ключевых отличиях архитектуры:
ТаблицаЗначений не может существовать на клиенте и напрямую отображаться на управляемой форме. Ему должен соответствовать реквизит формы с типом «Таблица части» (или коллекция данных формы).ИзменитьРеквизиты(). Процесс состоит из трех шагов: сначала создаются новые реквизиты формы (объекты класса РеквизитФормы), затем вызывается ИзменитьРеквизиты() для их добавления в контекст формы, и только после этого программно создаются элементы интерфейса (поля ввода) на форме для визуального отображения колонок.Понимание этих нюансов позволит вам писать надежный код, который легко масштабируется и не вызывает досадных ошибок переполнения диапазонов индексов.