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

Программист 1С v8.3 (Обычные формы) Управленческий учет Промышленность, строительство и АПК
← На главную

Каждый разработчик 1С в своей практической деятельности сталкивался со стандартным, но от этого не менее неприятным сбоем при проведении документов — ошибкой «Индекс находится за границами массива». Это критическая ошибка выполнения, которая полностью останавливает транзакцию проведения и блокирует работу пользователей в программе.

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

Выясняем причину: почему система выдает ошибку индексации?

Для того чтобы понять природу возникновения этой ошибки, давайте вспомним базовые правила работы с коллекциями в платформе «1С:Предприятие». Массив — это линейная последовательность элементов, доступ к которым осуществляется по порядковому номеру (индексу). Ключевое правило здесь звучит так: индексация элементов в массиве всегда начинается с 0.

Рассмотрим структуру индексов на простом примере:

  1. Первый элемент коллекции имеет индекс 0.
  2. Второй элемент коллекции имеет индекс 1.
  3. Третий элемент коллекции имеет индекс 2, и так далее.

Если прикладной алгоритм ожидает получить массив определенной длины и сразу обращается к элементу по индексу 1 (то есть ко второму элементу), но в результате выполнения функции возвращается массив, состоящий всего из одного элемента (доступного только по индексу 0), происходит аварийная остановка системы с сообщением «Индекс находится за границами массива».

Разбираем исходный код проведения документа

Давайте подробно проанализируем фрагмент кода, в котором происходит ошибка проведения. Этот код отвечает за запись данных о выполненной работе в регистры накопления ОбъемРаботы и ОбъемРаботыДляЗарплаты:


// Данный фрагмент построен конструктором движений и доработан вручную
Движения.ОбъемРаботы.Записывать = Истина;
Движения.ОбъемРаботыДляЗарплаты.Записывать = Истина;

Для Каждого ТекСтрокаТабличнаяЧасть Из ТабличнаяЧасть Цикл
    Сотры = ВзятьИзРегистра(ТекСтрокаТабличнаяЧасть.Станок, ТекСтрокаТабличнаяЧасть.Смена);
    
    Если Сотры[1] <> Справочники.Сотрудники.ПустаяСсылка Тогда
        Движение = Движения.ОбъемРаботы.Добавить();
        Движение.Период = Дата;
        Движение.Сотрудник = Сотры[1];
        Движение.ТипРаботы = Справочники.ТипРаботы.Простой;
        Движение.Количество = ТекСтрокаТабличнаяЧасть.Простой * Сотры[3] / 100;
        Движение.Станок = ТекСтрокаТабличнаяЧасть.Станок;
        
        // ... запись во второй регистр ...
    КонецЕсли;
КонецЦикла;

В чем кроется главная ошибка разработчика, писавшего этот код? Он применил антипаттерн «жесткое ожидание структуры» (хардкод). В коде заложено допущение, что функция ВзятьИзРегистра() всегда возвращает массив, состоящий ровно из 4 элементов:

Пока на производстве в каждую смену за станком стабильно трудились два человека, данный алгоритм работал стабильно. Однако в реальной жизни происходят изменения: например, на станке остался работать только один сотрудник. В этот момент функция ВзятьИзРегистра() возвращает массив меньшей длины (например, состоящий только из элементов Сотры[0] и Сотры[2]). Попытка выполнить сравнение в строке Если Сотры[1] <> ... мгновенно обрушивает программу, так как элемента с индексом 1 физически нет в оперативной памяти.

Скрытые архитектурные проблемы в коде (анализ узких мест)

Помимо очевидной ошибки с индексацией, представленный фрагмент содержит грубейшие нарушения стандартов разработки «1С», которые критически влияют на общую производительность системы:

  1. Запросы внутри цикла. Функции ВзятьИзРегистра() и НайтиОплату() вызываются в теле цикла по строкам табличной части документа. Если в документе будет несколько сотен строк, система выполнит сотни отдельных обращений к базе данных. Это приведет к критическому замедлению проведения и блокировкам СУБД.
  2. Многократные вызовы одной и той же тяжелой функции. В оригинальном коде функция НайтиОплату() вызывается трижды в рамках одной итерации цикла с абсолютно идентичными параметрами. Вместо этого результат функции необходимо кэшировать в локальную переменную.
  3. Слабая масштабируемость бизнес-логики. Алгоритм жестко завязан на то, что сотрудников может быть максимум двое. Если бизнес-процессы предприятия изменятся, и на один станок в смену потребуется назначить трех или четырех сотрудников, этот код придется полностью переписывать.

Как локализовать ошибку с помощью отладчика?

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

  1. Откроем конфигуратор нашей информационной базы и перейдем в модуль документа, где расположен проблемный обработчик проведения.
  2. Установим точку останова (клавиша F9) на строке вызова функции: Сотры = ВзятьИзРегистра(...).
  3. Запустим сеанс «1С:Предприятие» в режиме отладки и попробуем провести тот документ, на котором возникает ошибка.
  4. После того как выполнение программы остановится на нашей точке останова, нажмем клавишу F10 (Шагнуть в обход), чтобы выполнилась строка получения данных.
  5. Выделим переменную Сотры мышкой и нажмем комбинацию клавиш Shift + F9 (Вычислить выражение).
  6. В открывшемся табло мы увидим точное число элементов в массиве. Мы сможем лично зафиксировать, что при возникновении сбоя размер массива составляет меньше 4 элементов, что и вызывает исключительную ситуацию.

Способ 1. Быстрое устранение ошибки (проверка длины массива)

Если вам необходимо восстановить работоспособность учетной системы в кратчайшие сроки («здесь и сейчас»), самым простым и надежным решением станет добавление принудительной проверки количества элементов в массиве перед обращением к конкретным индексам. Для этого мы будем использовать метод коллекции Количество().

Давайте посмотрим, как будет выглядеть доработанный и безопасный фрагмент кода:


Для Каждого ТекСтрокаТабличнаяЧасть Из ТабличнаяЧасть Цикл
    Сотры = ВзятьИзРегистра(ТекСтрокаТабличнаяЧасть.Станок, ТекСтрокаТабличнаяЧасть.Смена);
    
    // Проверяем, что в массиве достаточно элементов для безопасной обработки второго сотрудника
    Если Сотры.Количество() >= 4 Тогда
        Если ЗначениеЗаполнено(Сотры[1]) Тогда
            Движение = Движения.ОбъемРаботы.Добавить();
            Движение.Период = Дата;
            Движение.Сотрудник = Сотры[1];
            Движение.ТипРаботы = Справочники.ТипРаботы.Простой;
            Движение.Количество = ТекСтрокаТабличнаяЧасть.Простой * Сотры[3] / 100;
            Движение.Станок = ТекСтрокаТабличнаяЧасть.Станок;
            // ... остальная логика записи движений ...
        КонецЕсли;
    КонецЕсли;
    
    // Проверяем наличие элементов для первого сотрудника (индексы 0 и 2)
    Если Сотры.Количество() >= 3 Тогда
        Если ЗначениеЗаполнено(Сотры[0]) Тогда
            Движение = Движения.ОбъемРаботы.Добавить();
            Движение.Период = Дата;
            Движение.Сотрудник = Сотры[0];
            Движение.ТипРаботы = Справочники.ТипРаботы.Простой;
            Движение.Количество = ТекСтрокаТабличнаяЧасть.Простой * Сотры[2] / 100;
            Движение.Станок = ТекСтрокаТабличнаяЧасть.Станок;
            // ... остальная логика записи движений ...
        КонецЕсли;
    КонецЕсли;
КонецЦикла;

Этот способ гарантированно устранит ошибку «Индекс находится за границами массива», однако он является лишь временной мерой, поскольку сохраняет проблемы низкой производительности из-за запросов в цикле.

Способ 2. Рефакторинг кода: переход на динамическую структуру коллекции

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

Давайте посмотрим, как изящно и надежно выглядит обработка такой динамической коллекции в цикле проведения:


Для Каждого ТекСтрокаТабличнаяЧасть Из ТабличнаяЧасть Цикл
    // Теперь функция возвращает массив структур: Новый Структура("Сотрудник, ДоляРаспределения")
    СписокСотрудников = ВзятьИзРегистра(ТекСтрокаТабличнаяЧасть.Станок, ТекСтрокаТабличнаяЧасть.Смена);
    
    // Динамический обход коллекции автоматически решает проблему любого количества сотрудников
    Для Каждого ДанныеСотрудника Из СписокСотрудников Цикл
        
        Если Не ЗначениеЗаполнено(ДанныеСотрудника.Сотрудник) Тогда
            Продолжить; // Пропускаем незаполненные строки
        КонецЕсли;
        
        // 1. Формируем движение в регистр ОбъемРаботы
        Движение = Движения.ОбъемРаботы.Добавить();
        Движение.Период = Дата;
        Движение.Сотрудник = ДанныеСотрудника.Сотрудник;
        Движение.ТипРаботы = Справочники.ТипРаботы.Простой;
        Движение.Количество = ТекСтрокаТабличнаяЧасть.Простой * ДанныеСотрудника.ДоляРаспределения / 100;
        Движение.Станок = ТекСтрокаТабличнаяЧасть.Станок;
        
        // Однократно получаем тариф оплаты и кэшируем его в переменную
        ТарифОплаты = НайтиОплату(ДанныеСотрудника.Сотрудник, Справочники.ТипРаботы.Простой, ТекСтрокаТабличнаяЧасть.Станок);
        
        // 2. Формируем движение в регистр ОбъемРаботыДляЗарплаты
        Движение2 = Движения.ОбъемРаботыДляЗарплаты.Добавить();
        Движение2.Период = Дата;
        Движение2.Сотрудник = ДанныеСотрудника.Сотрудник;
        Движение2.Отдел = Справочники.Должность.ТкацкийЦех;
        
        Если ЗначениеЗаполнено(ТарифОплаты) И ТарифОплаты <> 0 Тогда
            Движение2.Сумма = Движение.Количество * ТарифОплаты;
        Иначе
            Движение2.Сумма = 0;
        КонецЕсли;
        
    КонецЦикла;
КонецЦикла;

С внедрением такого подхода наш алгоритм стал абсолютно универсальным. Будет ли на станке работать 1 сотрудник, 2 сотрудника или целая бригада из 5 человек — код отработает корректно и без ошибок.

Способ 3. Максимальная оптимизация (пакетная выборка запросом вне цикла)

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

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

Рассмотрим пример полной высокопроизводительной реализации процедуры проведения документа:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    Движения.ОбъемРаботы.Записывать = Истина;
    Движения.ОбъемРаботыДляЗарплаты.Записывать = Истина;
    
    // Шаг 1. Подготавливаем уникальные ключи для выборки из базы данных
    ТаблицаКлючей = ТабличнаяЧасть.Выгрузить();
    ТаблицаКлючей.Свернуть("Станок, Смена");
    
    // Шаг 2. Создаем и выполняем пакетный запрос
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ВТ_Ключи.Станок КАК Станок,
        |	ВТ_Ключи.Смена КАК Смена
        |ПОМЕСТИТЬ ВТ_Ключи
        |ИЗ
        |	&ТаблицаКлючей КАК ВТ_Ключи
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |	СотрудникиСтанков.Станок КАК Станок,
        |	СотрудникиСтанков.Смена КАК Смена,
        |	СотрудникиСтанков.Сотрудник КАК Сотрудник,
        |	СотрудникиСтанков.ПроцентДоли КАК ПроцентДоли,
        |	ЕСТЬNULL(ТарифыОплаты.Тариф, 0) КАК ТарифОплаты
        |ИЗ
        |	РегистрСведений.СотрудникиНаСтанках КАК СотрудникиСтанков
        |		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Ключи КАК ВТ_Ключи
        |		ПО СотрудникиСтанков.Станок = ВТ_Ключи.Станок
        |			И СотрудникиСтанков.Смена = ВТ_Ключи.Смена
        |		ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ТарифыОплатыСотрудников КАК ТарифыОплаты
        |		ПО СотрудникиСтанков.Сотрудник = ТарифыОплаты.Сотрудник
        |			И ТарифыОплаты.ТипРаботы = &ТипПростой
        |			И СотрудникиСтанков.Станок = ТарифыОплаты.Станок";
    
    Запрос.УстановитьПараметр("ТаблицаКлючей", ТаблицаКлючей);
    Запрос.УстановитьПараметр("ТипПростой", Справочники.ТипРаботы.Простой);
    
    РезультатЗапроса = Запрос.Выполнить();
    Если РезультатЗапроса.Пустой() Тогда
        Возврат; // Данные для распределения отсутствуют в системе
    КонецЕсли;
    
    // Шаг 3. Индексируем полученные данные в Соответствие
    ДанныеДляРаспределения = Новый Соответствие;
    Выборка = РезультатЗапроса.Выбрать();
    Пока Выборка.Следующий() Цикл
        
        КлючСвязи = Строка(Выборка.Станок.УникальныйИдентификатор()) + "_" + Строка(Выборка.Смена.УникальныйИдентификатор());
        
        СписокСотрудниковСмены = ДанныеДляРаспределения.Получить(КлючСвязи);
        Если СписокСотрудниковСмены = Неопределено Тогда
            СписокСотрудниковСмены = Новый Массив;
            ДанныеДляРаспределения.Вставить(КлючСвязи, СписокСотрудниковСмены);
        КонецЕсли;
        
        НовыйСотрудник = Новый Структура;
        НовыйСотрудник.Вставить("Сотрудник", Выборка.Сотрудник);
        НовыйСотрудник.Вставить("ПроцентДоли", Выборка.ПроцентДоли);
        НовыйСотрудник.Вставить("ТарифОплаты", Выборка.ТарифОплаты);
        
        СписокСотрудниковСмены.Добавить(НовыйСотрудник);
    КонецЦикла;
    
    // Шаг 4. Основной цикл проведения документа работает мгновенно в ОЗУ
    Для Каждого ТекСтрокаТабличнаяЧасть Из ТабличнаяЧасть Цикл
        
        КлючСвязи = Строка(ТекСтрокаТабличнаяЧасть.Станок.УникальныйИдентификатор()) + "_" + Строка(ТекСтрокаТабличнаяЧасть.Смена.УникальныйИдентификатор());
        СписокСотрудников = ДанныеДляРаспределения.Получить(КлючСвязи);
        
        Если СписокСотрудников = Неопределено Тогда
            Продолжить; // По данному станку и смене нет назначенных сотрудников
        КонецЕсли;
        
        Для Каждого ДанныеСотрудника Из СписокСотрудников Цикл
            
            КоличествоПростоя = ТекСтрокаТабличнаяЧасть.Простой * ДанныеСотрудника.ПроцентДоли / 100;
            
            // Заполняем движение регистра ОбъемРаботы
            Движение = Движения.ОбъемРаботы.Добавить();
            Движение.Период = Дата;
            Движение.Сотрудник = ДанныеСотрудника.Сотрудник;
            Движение.ТипРаботы = Справочники.ТипРаботы.Простой;
            Движение.Количество = КоличествоПростоя;
            Движение.Станок = ТекСтрокаТабличнаяЧасть.Станок;
            
            // Заполняем движение регистра ОбъемРаботыДляЗарплаты
            Движение2 = Движения.ОбъемРаботыДляЗарплаты.Добавить();
            Движение2.Период = Дата;
            Движение2.Сотрудник = ДанныеСотрудника.Сотрудник;
            Движение2.Отдел = Справочники.Должность.ТкацкийЦех;
            Движение2.Сумма = КоличествоПростоя * ДанныеСотрудника.ТарифОплаты;
            
        КонецЦикла;
        
    КонецЦикла;

КонецПроцедуры

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

← На главную