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