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