Циклы во встроенном языке 1С 8.3: синтаксис, выбор и скорость
Содержание
Во встроенном языке 1С ровно три типа циклов: Для ... По (счётчик с шагом 1), Для Каждого ... Из (перебор элементов коллекции) и Пока (повтор, пока условие истинно). Все три заканчиваются словом КонецЦикла;, а управлять ими изнутри можно двумя операторами — Прервать и Продолжить.
Частое заблуждение новичков — искать в 1С цикл с шагом -1 или конструкцию «выполнять, пока не…» (do-while). Их нет. Обратный проход и цикл с проверкой в конце собираются из тех же трёх видов — ниже показано как. Всё сказанное относится к платформе 1С:Предприятие 8.3 и одинаково работает в любой конфигурации: БП, ЗУП, УТ или своей разработке.
Три вида циклов в 1С и когда какой выбирать
Выбор цикла почти всегда определяется тем, что именно вы перебираете.
| Цикл | Синтаксис | Когда брать | Подводный камень |
|---|---|---|---|
| Для | Для Сч = 1 По 10 Цикл | Нужен номер итерации, индекс массива, фиксированное число повторов | Шаг только +1, границы считаются один раз |
| Для каждого | Для Каждого Эл Из Массив Цикл | Перебор массива, таблицы значений, структуры, соответствия, табличной части | Нельзя удалять элементы перебираемой коллекции |
| Пока | Пока Условие Цикл | Число повторов заранее неизвестно: выборка запроса, чтение файла, поиск до совпадения | Легко получить бесконечный цикл |
Простое правило:
- 📦 есть коллекция и номер элемента не нужен —
Для Каждого; - 🔢 нужен индекс или счёт «от и до» —
Для; - 🔁 условие выхода появляется по ходу работы —
Пока.
Цикл «Для»: перебор по номеру от и до
Синтаксис:
Для Счетчик = НачальноеЗначение По КонечноеЗначение Цикл
// тело цикла
КонецЦикла;
Переменная-счётчик увеличивается на 1 после каждого прохода. Цикл выполняется, пока счётчик не превысит конечное значение. Если начальное значение больше конечного, тело не выполнится ни разу — ошибки при этом не будет.
Пример: сумма чисел от 1 до 100.
Сумма = 0;
Для Н = 1 По 100 Цикл
Сумма = Сумма + Н;
КонецЦикла;
// Сумма = 5050
Перебор массива по индексу
Индексы массива в 1С начинаются с нуля. Верхнюю границу удобно брать через ВГраница(): у пустого массива она равна -1, и цикл просто не стартует.
Для Индекс = 0 По Массив.ВГраница() Цикл
Сообщить("Элемент " + Формат(Индекс, "ЧГ=0") + ": " + Массив[Индекс]);
КонецЦикла;
Число со строкой склеивайте через Формат(Число, "ЧГ=0") или СтрШаблон(). Иначе при значениях от 1 000 в текст попадёт разделитель групп разрядов: «1 000» вместо «1000».
Обратный проход без шага -1
Ключевого слова для шага в 1С нет. Обратный порядок получают пересчётом индекса:
Количество = Массив.Количество();
Для Сч = 1 По Количество Цикл
Индекс = Количество - Сч; // идёт от Количество-1 до 0
Элемент = Массив[Индекс];
КонецЦикла;
Именно такой проход нужен, когда из массива по ходу перебора удаляются элементы — подробности в разделе об ошибках.
Что ещё знать про «Для»
- ⚙️ Начальное и конечное значения вычисляются один раз, в начале цикла. Если внутри изменить переменную, из которой взята граница, число итераций не поменяется.
- ✏️ Счётчик внутри тела можно изменить вручную, но так код становится нечитаемым. Нужен нестандартный шаг — берите
Пока. - 📏 Цикл с шагом 2, 5 или 10 проще всего записать через умножение:
Для Н = 0 По 9 Цикл Значение = Н * 5; ....
Цикл «Для каждого»: работа с коллекциями 1С
Синтаксис:
Для Каждого Элемент Из Коллекция Цикл
// Элемент — текущий элемент коллекции
КонецЦикла;
Это самый частый цикл в прикладном коде. Он не требует счётчика и не даст выйти за границы коллекции.
Что оказывается в переменной Элемент для разных коллекций:
| Коллекция | Тип элемента | Как обращаться |
|---|---|---|
| Массив | значение элемента | Элемент |
| ТаблицаЗначений | СтрокаТаблицыЗначений | Элемент.ИмяКолонки |
| Табличная часть документа | строка табличной части | Элемент.Номенклатура, Элемент.Количество |
| СписокЗначений | ЭлементСпискаЗначений | Элемент.Значение, Элемент.Представление |
| Структура, Соответствие | КлючИЗначение | Элемент.Ключ, Элемент.Значение |
| ДеревоЗначений | перебирают Дерево.Строки | у каждой строки свои .Строки |
Пример: итог по колонке табличной части.
ИтогСумма = 0;
Для Каждого СтрокаТЧ Из Объект.Товары Цикл
ИтогСумма = ИтогСумма + СтрокаТЧ.Сумма;
КонецЦикла;
Для такой задачи есть и готовый метод — Объект.Товары.Итог("Сумма"). Он короче, а цикл оставьте для случаев с условием отбора внутри.
Пример: перебор соответствия.
Для Каждого КлючЗначение Из Соответствие Цикл
Сообщить(Строка(КлючЗначение.Ключ) + " = " + Строка(КлючЗначение.Значение));
КонецЦикла;
Дерево значений: рекурсия вместо вложенных циклов
Глубина дерева заранее неизвестна, поэтому вложенные циклы тут не помогают. Пишут процедуру, которая вызывает сама себя:
Процедура ОбойтиСтроки(Строки, Уровень)
Для Каждого Строка Из Строки Цикл
Сообщить(Формат(Уровень, "ЧГ=0") + ": " + Строка.Наименование);
ОбойтиСтроки(Строка.Строки, Уровень + 1);
КонецЦикла;
КонецПроцедуры
ОбойтиСтроки(Дерево.Строки, 1);
Для Каждого перебирает коллекцию в оперативной памяти. Результат запроса так не обходят: для ВыборкаИзРезультатаЗапроса используется цикл Пока Выборка.Следующий().
Цикл «Пока»: повтор по условию
Синтаксис:
Пока Условие Цикл
// тело выполняется, пока Условие = Истина
КонецЦикла;
Условие проверяется перед каждым проходом. Если оно сразу ложно, тело не выполнится ни разу.
Обход выборки запроса
Самый частый случай применения «Пока» в 1С:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование
|ИЗ
| Справочник.Номенклатура КАК Номенклатура";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Наименование);
КонецЦикла;
Метод Следующий() сдвигает выборку на одну запись и возвращает Ложь, когда записи кончились. Отдельный счётчик не нужен.
Цикл с проверкой в конце
Конструкции «повторять до» в языке нет. Её заменяет бесконечный цикл с выходом по условию:
НомерПопытки = 0;
Пока Истина Цикл
// действие выполняется хотя бы один раз
НомерПопытки = НомерПопытки + 1;
Если Успех Или НомерПопытки >= 5 Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Каждый цикл Пока Истина должен иметь гарантированный выход — счётчик попыток, ограничение по времени или по числу записей. Иначе сеанс зависнет и на сервере будет занимать рабочий процесс.
Нестандартный шаг
Н = 100;
Пока Н > 0 Цикл
// Н = 100, 90, 80 ... 10
Н = Н - 10;
КонецЦикла;
Прервать и Продолжить: управление ходом цикла
Оба оператора работают в любом из трёх видов циклов.
- ⛔
Прервать— немедленно выходит из цикла. Выполнение продолжается со строки послеКонецЦикла. - ⏭️
Продолжить— пропускает остаток тела и переходит к следующей итерации. В «Для» счётчик при этом увеличится, в «Для каждого» возьмётся следующий элемент.
Пример: ищем первую строку с нулевой ценой и пропускаем услуги.
НайденнаяСтрока = Неопределено;
Для Каждого СтрокаТЧ Из Объект.Товары Цикл
Если СтрокаТЧ.ЭтоУслуга Тогда
Продолжить;
КонецЕсли;
Если СтрокаТЧ.Цена = 0 Тогда
НайденнаяСтрока = СтрокаТЧ;
Прервать;
КонецЕсли;
КонецЦикла;
Здесь ЭтоУслуга — условная колонка для примера; в вашей табличной части признак может называться иначе.
Выход сразу из двух циклов
Прервать завершает только тот цикл, внутри которого стоит, — самый внутренний. Меток для циклов в языке нет. Рабочих приёмов два:
- Флаг. Во внутреннем цикле ставите
Найдено = ИстинаиПрервать, во внешнем сразу после внутреннего проверяетеЕсли Найдено Тогда Прервать; КонецЕсли;. - Отдельная функция. Вложенные циклы выносятся в функцию, а выход делается через
Возврат. Он завершает функцию целиком, сколько бы циклов ни было открыто. Этот вариант читается лучше.
Оператор безусловного перехода Перейти ~Метка; в языке есть, но для выхода из циклов его не используют: код с переходами тяжело сопровождать.
Вложенные циклы и оптимизация кода
Сам по себе цикл в 1С работает быстро. Медленным его делает то, что происходит внутри. Главные источники тормозов:
1. Запрос или обращение к базе внутри цикла
Каждый Запрос.Выполнить() и каждое чтение реквизита через точку от ссылки (СтрокаТЧ.Номенклатура.ЕдиницаИзмерения) — это обращение к базе данных. В цикле на 5 000 строк получаются тысячи отдельных обращений.
Решение: собрать нужные данные одним запросом до цикла, а внутри брать их из выборки или соответствия.
2. Вложенный цикл «каждый с каждым»
Сопоставление двух таблиц по 1 000 строк двумя вложенными циклами — это 1 000 × 1 000 = 1 000 000 сравнений. Варианты лучше:
- 🗂️ сначала сложить вторую таблицу в
Соответствиепо ключу, а затем одним циклом брать значение черезСоответствие.Получить(Ключ); - 🔍 добавить индекс в таблицу значений:
Таблица.Индексы.Добавить("Номенклатура"), после чегоТаблица.НайтиСтроки(Новый Структура("Номенклатура", Ссылка))работает по индексу; - 🧩 перенести сопоставление в запрос — соединение таблиц на стороне СУБД обычно быстрее любого цикла.
// вместо вложенного цикла
Цены = Новый Соответствие;
Для Каждого СтрокаЦены Из ТаблицаЦен Цикл
Цены.Вставить(СтрокаЦены.Номенклатура, СтрокаЦены.Цена);
КонецЦикла;
Для Каждого СтрокаТЧ Из Объект.Товары Цикл
Цена = Цены.Получить(СтрокаТЧ.Номенклатура);
Если Цена <> Неопределено Тогда
СтрокаТЧ.Цена = Цена;
КонецЕсли;
КонецЦикла;
3. Поиск в массиве внутри цикла
Массив.Найти() и ТаблицаЗначений.Найти() без индекса просматривают коллекцию от начала. В цикле это тот же скрытый вложенный перебор. Для проверки «есть ли значение» используйте соответствие.
4. Вызов сервера из клиентского цикла
Если клиентский код в цикле вызывает серверную процедуру, каждая итерация становится отдельным вызовом сервера. Передайте на сервер весь массив данных одним вызовом и крутите цикл там.
5. Склейка длинной строки
Многократное Текст = Текст + ... на десятках тысяч итераций заметно тормозит. Складывайте части в массив, а в конце соберите строку функцией СтрСоединить(Массив, Символы.ПС).
Какой цикл быстрее: сравнение производительности
На простых операциях с коллекцией в памяти разница между «Для» по индексу и «Для каждого» незначительна. На реальных задачах она теряется на фоне обращений к базе. Выбирайте цикл по читаемости, а не по скорости.
Что действительно влияет на время:
| Что внутри цикла | Влияние на скорость |
|---|---|
| Арифметика, присваивания, работа с переменными | минимальное |
Соответствие.Получить(), поиск по индексированной таблице | небольшое |
Массив.Найти(), Найти() в таблице без индекса | растёт вместе с размером коллекции |
| Вызов сервера с клиента | заметное, на каждую итерацию |
| Запрос, запись объекта, чтение реквизита через точку | самое большое |
Если сомневаетесь, замерьте сами. Функция ТекущаяУниверсальнаяДатаВМиллисекундах() возвращает время в миллисекундах:
Начало = ТекущаяУниверсальнаяДатаВМиллисекундах();
// проверяемый цикл
Длительность = ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало;
Сообщить("Цикл выполнялся " + Формат(Длительность, "ЧГ=0; ЧН=0") + " мс");
Для полноценного анализа есть замер производительности в отладчике Конфигуратора. Он показывает, сколько времени заняла каждая строка модуля, и сразу выводит самые тяжёлые места.
Длинную обработку в толстом клиенте пользователь может прервать сочетанием Ctrl+Break, если в теле цикла вызывается ОбработкаПрерыванияПользователя(). Без этого вызова прервать цикл с клавиатуры не получится.
Частые ошибки в циклах 1С
Правильное удаление строк таблицы значений по условию:
КУдалению = Новый Массив;
Для Каждого Строка Из Таблица Цикл
Если Строка.Количество = 0 Тогда
КУдалению.Добавить(Строка);
КонецЕсли;
КонецЦикла;
Для Каждого Строка Из КУдалению Цикл
Таблица.Удалить(Строка);
КонецЦикла;
Здесь второй цикл перебирает вспомогательный массив, а удаляет из таблицы. Перебираемая коллекция при этом не меняется, и строки не теряются.
Частые вопросы
Есть ли в 1С цикл с шагом, например 2 или -1?
Н * 2), пересчётом индекса для обратного порядка или циклом «Пока» с ручным изменением переменной.Можно ли получить номер элемента в цикле «Для каждого»?
Таблица.Индекс(Строка).Можно ли изменять поля строки внутри «Для каждого»?
Чем Прервать отличается от Возврат?
Прервать выходит только из текущего цикла, и процедура продолжает работу дальше. Возврат завершает всю процедуру или функцию, включая все открытые циклы.Работают ли циклы в языке запросов 1С?
Пока Выборка.Следующий() или после выгрузки в таблицу значений.