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

· 10 мин

Содержание

Во встроенном языке 1С три вида циклов: Для … По … Цикл со счётчиком, Для Каждого … Из … Цикл для обхода коллекций и Пока … Цикл с условием. Каждый закрывается словом КонецЦикла;, досрочный выход — Прервать;, переход к следующей итерации — Продолжить;. Цикла с постусловием и шага у счётчика в языке нет: их заменяют конструкцией Пока.

Сам цикл в 1С почти никогда не бывает медленным. Тормозит то, что внутри него: запрос к базе, обращение к реквизиту через точку, вызов сервера с клиента, запись объекта. Ниже — как работает цикл в платформе, какие конструкции выбрать и как переписать типичный медленный код. Всё относится к платформе 8.3 и не зависит от конфигурации.

Как платформа выполняет цикл и где тратится время

Код модуля платформа компилирует в байт-код и исполняет на той стороне, где он вызван: на клиенте или на сервере. Цикл — это просто повтор блока команд. Его собственные накладные расходы малы: пустой цикл на миллион итераций проходит за доли секунды.

Время уходит на операции, которые цикл повторяет:

  • 🗄️ обращение к базе данных — запрос, Ссылка.Реквизит, ПолучитьОбъект(), Записать(). Каждое — отдельный поход к СУБД;
  • 🌐 вызов сервера с клиента — каждая процедура с директивой &НаСервере или функция серверного общего модуля, вызванная из клиентского цикла, означает передачу данных по сети туда и обратно;
  • 🧮 поиск в коллекции перебором — Найти() в массиве или таблице без индекса просматривает все элементы, и цикл превращается в «цикл в цикле»;
  • 🧵 склеивание строк — Текст = Текст + … на десятках тысяч итераций каждый раз создаёт новую строку.

Отсюда главное правило: сначала соберите данные одним запросом, потом обрабатывайте их в памяти. Цикл по таблице значений из 100 000 строк работает секунды. Те же 100 000 запросов в цикле — минуты и часы.

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

Синтаксис циклов 1С: три конструкции и операторы управления

Цикл со счётчиком Для … По

Подходит, когда известно число повторов или нужен индекс элемента.

Для Сч = 1 По 10 Цикл
    Сообщить("Итерация " + Формат(Сч, "ЧГ=0"));
КонецЦикла;

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

ВГраница = Массив.ВГраница();
Для Индекс = 0 По ВГраница Цикл
    Значение = Массив[Индекс];
КонецЦикла;

ВГраница() у пустого массива возвращает −1, и цикл не выполнится ни разу — отдельная проверка не нужна.

Обход коллекции Для Каждого … Из

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

Для Каждого СтрокаТЧ Из Объект.Товары Цикл
    СтрокаТЧ.Сумма = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
КонецЦикла;

У структуры и соответствия элемент обхода — пара КлючИЗначение:

Для Каждого Пара Из Параметры Цикл
    Сообщить(Пара.Ключ + " = " + Строка(Пара.Значение));
КонецЦикла;

Цикл с условием Пока

Выполняется, пока условие истинно. Нужен, когда число повторов заранее неизвестно: чтение выборки запроса, перебор дат, чтение файла.

Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
    Сообщить(Выборка.Наименование);
КонецЦикла;

Перебор дней периода — тоже через Пока, потому что у Для нет шага:

ТекДата = НачалоДня(ДатаНачала);
Пока ТекДата <= ДатаОкончания Цикл
    // обработка дня
    ТекДата = ТекДата + 86400;  // 86 400 секунд = сутки
КонецЦикла;

Цикл с постусловием эмулируют так: Пока Истина Цикл … Если <условие выхода> Тогда Прервать; КонецЕсли; КонецЦикла;.

Прервать и Продолжить

ОператорЧто делаетТипичный случай
Прервать;Выходит из текущего цикла целикомНашли нужный элемент, дальше искать не надо
Продолжить;Пропускает остаток тела, переходит к следующей итерацииСтрока не подходит под отбор
ВозвратВыходит из процедуры или функции, а значит, и из всех цикловРезультат найден во вложенном цикле

Оба оператора действуют только на ближайший цикл. Чтобы выйти из двух вложенных сразу, вынесите их в отдельную функцию и используйте Возврат. Оператор Перейти ~Метка; в языке есть, но в серверном и типовом коде его не применяют: читать такой код трудно.

Оптимизация циклов в 1С при обработке больших объёмов данных

Запрос вместо запросов в цикле

Классический медленный код — запрос на каждой строке табличной части:

// Плохо: N строк = N запросов к базе
Для Каждого Стр Из Объект.Товары Цикл
    Запрос = Новый Запрос("ВЫБРАТЬ Цены.Цена ИЗ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, Номенклатура = &Номенклатура) КАК Цены");
    Запрос.УстановитьПараметр("Номенклатура", Стр.Номенклатура);
    // ...
КонецЦикла;

Правильно — один запрос по всем товарам сразу, через условие В (&Список) или временную таблицу, а результат положить в соответствие:

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|    Цены.Номенклатура КАК Номенклатура,
|    Цены.Цена КАК Цена
|ИЗ
|    РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, Номенклатура В (&Список)) КАК Цены";
Запрос.УстановитьПараметр("Список", Объект.Товары.ВыгрузитьКолонку("Номенклатура"));

Цены = Новый Соответствие;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    Цены.Вставить(Выборка.Номенклатура, Выборка.Цена);
КонецЦикла;

Для Каждого Стр Из Объект.Товары Цикл
    Стр.Цена = Цены.Получить(Стр.Номенклатура);
КонецЦикла;

Имя регистра цен и его измерения в вашей конфигурации могут быть другими — сам приём от этого не меняется.

Реквизиты через точку

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

Решение — получить нужные поля в том же запросе, что и основные данные. В конфигурациях на БСП для этого есть ОбщегоНазначения.ЗначенияРеквизитовОбъектов(МассивСсылок, "Поле1, Поле2") — функция читает реквизиты пачки ссылок одним запросом.

Поиск и сборка строк

  • 🔎 Многократный поиск по таблице значений — добавьте индекс: Таблица.Индексы.Добавить("Номенклатура");, затем НайтиСтроки(). Для поиска по ключу удобнее Соответствие.
  • 🧵 Длинный текст собирайте в массив и склеивайте один раз: СтрСоединить(Массив, Символы.ПС) (платформа 8.3.6 и выше).
  • 📦 Миллион строк не выгружайте в таблицу через Выгрузить(), если они нужны один раз по порядку. Выборка.Следующий() читает данные порциями и бережёт память сервера.

Как измерить, что именно тормозит

Не угадывайте. Включите в конфигураторе Отладка → Замер производительности, выполните операцию и посмотрите, какие строки съели время. Как правило, почти всё время съедают одна-две строки внутри цикла. Быстрый замер прямо в коде:

Старт = ТекущаяУниверсальнаяДатаВМиллисекундах();
// ... цикл ...
Сообщить("Мс: " + Формат(ТекущаяУниверсальнаяДатаВМиллисекундах() - Старт, "ЧГ=0"));
Проверка цикла перед выпуском в рабочую базу
Выполнено: 0 / 7

Транзакции и блокировки при записи объектов в цикле

Каждый вызов Записать() вне явной транзакции платформа оборачивает в свою короткую транзакцию. Для 10 000 документов это 10 000 фиксаций — медленно, но безопасно для остальных пользователей.

Обратная крайность — одна транзакция на все 10 000 объектов. Она держит блокировки до самого конца, другие пользователи ждут, а при ошибке на 9 999-м объекте откатывается всё. Рабочий компромисс — порции:

РазмерПорции = 500;
Счетчик = 0;
НачатьТранзакцию();
Для Каждого Ссылка Из СписокДокументов Цикл
    ДокОбъект = Ссылка.ПолучитьОбъект();
    // ... изменения ...
    ДокОбъект.Записать();
    Счетчик = Счетчик + 1;
    Если Счетчик % РазмерПорции = 0 Тогда
        ЗафиксироватьТранзакцию();
        НачатьТранзакцию();
    КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();

Это упрощённая схема без обработки ошибок, полный вариант — в следующем разделе. Размер порции подбирают замером: 100–1000 объектов для документов, больше — для простых записей регистра сведений.

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

Управляемые блокировки внутри цикла

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

Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = ТаблицаТоваров;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();

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

Ошибки и исключения внутри циклической обработки

Одна битая строка не должна останавливать всё

Без обработки исключений первая же ошибка прерывает цикл, и непонятно, сколько объектов успело записаться. Заверните тело в Попытка … Исключение и пишите ошибку в журнал регистрации:

Для Каждого Ссылка Из СписокДокументов Цикл
    Попытка
        ДокОбъект = Ссылка.ПолучитьОбъект();
        ДокОбъект.Записать(РежимЗаписиДокумента.Проведение);
    Исключение
        ЗаписьЖурналаРегистрации("Перепроведение документов",
            УровеньЖурналаРегистрации.Ошибка, , Ссылка,
            ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
        Продолжить;
    КонецПопытки;
КонецЦикла;

Ловушка: исключение внутри общей транзакции

Если цикл идёт внутри одной открытой транзакции, перехват исключения не спасает. После ошибки записи транзакция уже помечена как неудачная, и следующая запись упадёт с текстом В данной транзакции уже происходили ошибки!. Обработка «продолжится», но ничего не запишет.

Правильная схема — своя транзакция на объект или на порцию, с откатом в Исключение:

Для Каждого Ссылка Из СписокДокументов Цикл
    НачатьТранзакцию();
    Попытка
        ДокОбъект = Ссылка.ПолучитьОбъект();
        ДокОбъект.Записать(РежимЗаписиДокумента.Проведение);
        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
        ЗаписьЖурналаРегистрации("Перепроведение документов",
            УровеньЖурналаРегистрации.Ошибка, , Ссылка,
            ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
    КонецПопытки;
КонецЦикла;

НачатьТранзакцию() ставят перед Попытка, а не внутри: так в блоке Исключение транзакция гарантированно открыта и её есть что отменять. Для порций логика та же, но при ошибке откатывается вся порция — её объекты потом обрабатывают повторно по одному.

Прерывание долгого цикла пользователем

В клиентском цикле вызовите ОбработкаПрерыванияПользователя(); — тогда пользователь сможет остановить его сочетанием Ctrl+Break. Прогресс показывают через Состояние("Обработано " + Формат(Сч, "ЧГ=0"), Процент). Серверную обработку на десятки минут лучше запускать фоновым заданием, а не держать клиента в ожидании.

Примеры циклов и антипаттерны из реальных обработок

Удаление строк из коллекции

Удалять элементы внутри Для Каждого нельзя: после удаления индексы сдвигаются, и цикл пропускает следующий элемент. Если подряд идут две строки на удаление, вторая из них останется.

Надёжный способ — обратный проход по индексам, с количеством, зафиксированным до начала цикла:

Всего = Таблица.Количество();
Для Сч = 1 По Всего Цикл
    Индекс = Всего - Сч;          // от Всего-1 вниз до 0
    Если Таблица[Индекс].Количество = 0 Тогда
        Таблица.Удалить(Индекс);
    КонецЕсли;
КонецЦикла;

При Всего = 5 индексы пойдут 4, 3, 2, 1, 0. Удаление строки 3 не сдвигает строки 0–2, которые ещё впереди. Для простых отборов есть вариант короче: собрать строки через НайтиСтроки() и удалить их в цикле по найденному массиву.

Сравнение подходов

ЗадачаМедленноБыстро
Цены по строкам документаЗапрос в каждой итерацииОдин запрос В (&Список) + Соответствие
Единица измерения товараСтр.Номенклатура.ЕдиницаИзмеренияПоле в запросе или ЗначенияРеквизитовОбъектов
Проверка «есть ли значение»Массив.Найти() в циклеСоответствие.Получить()
Запись движений регистраЗаписать() на каждую строкуОдин НаборЗаписей с отбором и одна запись
Отчёт-текст на 50 000 строкТекст = Текст + СтрокаМассив + СтрСоединить
Расчёт на форме по кнопкеСерверный вызов в каждой итерацииОдин вызов &НаСервере с циклом внутри

Бесконечный цикл

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

Защита для Пока на непредсказуемых данных — счётчик-предохранитель: Если Сч > 1000000 Тогда ВызватьИсключение "Превышено число итераций"; КонецЕсли;.

Изменение коллекции, по которой идёт обход

Добавлять строки в табличную часть внутри Для Каждого по той же табличной части — путь к непредсказуемому числу итераций. Новые строки собирайте в отдельный массив или таблицу и добавляйте после цикла.

Частые вопросы о циклах во встроенном языке

Можно ли в 1С задать шаг цикла Для, например 2?
Нет, счётчик всегда растёт на 1. Нужен шаг — используйте Пока с ручным приращением или пересчитывайте индекс в теле: Индекс = Сч * 2.
Что быстрее: Для Каждого или Для с индексом?
Для таблиц и массивов разница незаметна на фоне любых обращений к базе. Выбирайте по смыслу: Для Каждого проще читать, индекс нужен для обратного прохода и соседних элементов.
Почему нельзя обойти результат запроса через Для Каждого?
РезультатЗапроса не коллекция. Его обходят через Выбрать() и Пока Выборка.Следующий() или выгружают в таблицу значений методом Выгрузить(), и уже её перебирают Для Каждого.
Как выйти сразу из двух вложенных циклов?
Прервать выходит только из внутреннего. Вынесите оба цикла в функцию и используйте Возврат, либо ставьте флаг и проверяйте его во внешнем цикле.
Можно ли менять переменную-счётчик внутри цикла Для?
Технически присваивание допустимо, но такой код сложно читать и проверять. Если ход итераций зависит от данных, напишите Пока с явным условием.