Циклы во встроенном языке 1С 8.3: синтаксис, выбор и скорость

· 10 мин

Содержание

Во встроенном языке 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. Флаг. Во внутреннем цикле ставите Найдено = Истина и Прервать, во внешнем сразу после внутреннего проверяете Если Найдено Тогда Прервать; КонецЕсли;.
  2. Отдельная функция. Вложенные циклы выносятся в функцию, а выход делается через Возврат. Он завершает функцию целиком, сколько бы циклов ни было открыто. Этот вариант читается лучше.

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

Вложенные циклы и оптимизация кода

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