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

· 9 мин

Содержание

Перебрать таблицу значений в 1С 8.3 проще всего циклом Для Каждого Строка Из ТаблицаЗначений Цикл … КонецЦикла. Если нужен номер строки или строки удаляются по ходу, берите цикл по индексу: Для Индекс = 0 По Таблица.Количество() - 1 Цикл, а строку получайте через Таблица.Получить(Индекс) или Таблица[Индекс].

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

Как устроена таблица значений и что даёт перебор

Таблица значений (ТаблицаЗначений) — коллекция строк в памяти. У неё есть коллекция Колонки и сами строки типа СтрокаТаблицыЗначений. Нумерация строк начинается с нуля: первая строка — Таблица[0], последняя — Таблица[Таблица.Количество() - 1].

Обратиться к значению в строке можно двумя способами:

  • 🔹 через точку: Строка.Количество, если имя колонки известно при написании кода;
  • 🔹 через квадратные скобки: Строка["Количество"], если имя колонки хранится в переменной.

Таблица значений живёт только на сервере и в толстом клиенте. В управляемой форме реквизит с типом таблицы значений на клиенте виден как ДанныеФормыКоллекция. Перебирать его на клиенте можно тем же Для Каждого, но методы самой таблицы (Свернуть, ВыгрузитьКолонку, Скопировать) там недоступны. Для них реквизит переводят в значение на сервере функцией РеквизитФормыВЗначение.

Для перебора в языке есть три вида циклов. Каждый подходит под свою задачу:

ЦиклКогда братьЧто учесть
Для Каждого … Из … Циклчитаем или меняем значения в строкахнельзя удалять строки внутри цикла
Для Индекс = … По … Циклнужен номер строки, обратный обход, удалениеиндекс с нуля, граница Количество() - 1
Пока … Циклчисло итераций меняется по ходусчётчик ведём сами, легко зациклить

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

Перебор по индексу: метод Получить() и квадратные скобки

Классический вариант — счётчик от нуля до последнего индекса. Строку берут методом Получить() или обращением по индексу, разницы в результате нет.

Для Индекс = 0 По ТаблицаТоваров.Количество() - 1 Цикл
    Строка = ТаблицаТоваров.Получить(Индекс);
    // то же самое: Строка = ТаблицаТоваров[Индекс];
    Строка.Сумма = Строка.Количество * Строка.Цена;
КонецЦикла;

Граница Количество() - 1 вычисляется один раз, при входе в цикл. Если таблица пустая, граница равна −1, и цикл не выполнится ни разу. Ошибки не будет.

Цикл по индексу нужен, когда:

  • 📌 номер строки участвует в логике (каждая пятая строка, сравнение с соседней строкой);
  • 📌 нужно идти с конца — например, при удалении строк;
  • 📌 обработку надо начать не с первой строки.

Пример со сравнением соседних строк — отметить строку, если цена выросла относительно предыдущей:

Для Индекс = 1 По ТаблицаЦен.Количество() - 1 Цикл
    Текущая = ТаблицаЦен[Индекс];
    Предыдущая = ТаблицаЦен[Индекс - 1];
    Текущая.Рост = (Текущая.Цена > Предыдущая.Цена);
КонецЦикла;

Если нужно узнать номер строки, которую вы уже держите в руках, используйте Таблица.Индекс(Строка). Метод вернёт число от нуля или −1, если строки в таблице нет.

Номер строки для пользователя (в сообщении, в печатной форме) выводите как Индекс + 1. Склеивать число со строкой лучше через Формат(Индекс + 1, "ЧГ=0"), иначе на номере 1000 появится пробел-разделитель.

Цикл Для Каждого: короткий и читаемый обход строк

Для чтения и правки значений хватает цикла Для Каждого. Он не требует счётчика, и в нём не ошибёшься с границей.

ИтогоСумма = 0;
Для Каждого Строка Из ТаблицаТоваров Цикл
    Если Строка.Количество = 0 Тогда
        Продолжить;
    КонецЕсли;
    Строка.Сумма = Строка.Количество * Строка.Цена;
    ИтогоСумма = ИтогоСумма + Строка.Сумма;
КонецЦикла;

Переменная Строка — это ссылка на настоящую строку таблицы, а не её копия. Всё, что вы присвоите полям Строка, сразу окажется в таблице. Возвращать строку обратно не нужно.

Что можно делать внутри Для Каждого:

  • ✅ читать и менять значения в колонках;
  • ✅ выходить досрочно через Прервать, когда нужная строка найдена;
  • ✅ пропускать строки через Продолжить.

Чего делать нельзя:

  • ❌ удалять строки перебираемой таблицы — цикл перепрыгнет через часть строк;
  • ❌ добавлять строки в ту же таблицу — результат зависит от позиции вставки, такой код трудно отладить.

Обход колонок внутри строки

Когда структура таблицы заранее неизвестна (например, её вернула универсальная функция), перебирают и строки, и колонки:

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

Так удобно выгружать таблицу в текст, CSV или проверять, в каких колонках пустые значения.

Как удалять и пропускать строки при переборе

Здесь чаще всего и возникают ошибки. Есть два разных намерения, и решаются они по-разному.

Пропустить строку (не обрабатывать, но оставить в таблице) — это Продолжить в любом цикле.

Удалить строку из таблицы — только обходом с конца. Если идти с начала, после удаления строки 5 на её место встаёт строка 6, а счётчик уже переходит к 6, и бывшая шестая строка остаётся непроверенной.

Правильный обратный цикл:

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

Проверим на таблице из 4 строк: Индекс пройдёт значения 3, 2, 1, 0. Удаление строки 2 сдвигает только строку 3, а её мы уже проверили. Ничего не теряется.

Метод Удалить() принимает и индекс, и саму строку: ТаблицаТоваров.Удалить(Строка).

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

КУдалению = Новый Массив;
Для Каждого Строка Из ТаблицаТоваров Цикл
    Если Строка.Количество = 0 Тогда
        КУдалению.Добавить(Строка);
    КонецЕсли;
КонецЦикла;

Для Каждого Строка Из КУдалению Цикл
    ТаблицаТоваров.Удалить(Строка);
КонецЦикла;

Если условие отбора простое (равенство значений колонок), массив даст метод НайтиСтроки():

Отбор = Новый Структура("Количество", 0);
Для Каждого Строка Из ТаблицаТоваров.НайтиСтроки(Отбор) Цикл
    ТаблицаТоваров.Удалить(Строка);
КонецЦикла;

Здесь цикл идёт по массиву найденных строк, а не по самой таблице, поэтому удаление ничего не ломает.

Если вы не удаляете строки, а хотите получить из таблицы только нужные, удобнее сделать новую таблицу: Отобранные = ТаблицаТоваров.Скопировать(ТаблицаТоваров.НайтиСтроки(Отбор)). Исходная таблица останется целой.

Ошибка внутри итерации: Попытка … Исключение

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

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

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

Какой способ перебора быстрее

На простом переборе разница между Для Каждого и циклом по индексу невелика. Таблицу на десятки тысяч строк оба варианта проходят быстро, а время уходит на то, что вы делаете внутри цикла.

Что действительно тормозит:

Что внутри циклаПочему медленноЧем заменить
запрос к базе на каждой строкетысячи обращений к СУБДодин запрос с временной таблицей до цикла
Найти() или НайтиСтроки() по второй таблицеполный просмотр второй таблицы на каждой итерациииндекс по колонке или Соответствие
обращение через точку к реквизитам ссылки (Строка.Номенклатура.Артикул)скрытый запрос к базеполучить реквизит в запросе отдельной колонкой
Сообщить() на каждой строкевывод тысяч сообщенийсобрать текст и вывести один раз

Проверить свой случай можно замером:

Начало = ТекущаяУниверсальнаяДатаВМиллисекундах();
Для Каждого Строка Из Таблица Цикл
    // обработка
КонецЦикла;
Длительность = ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало;
Сообщить("Обход занял, мс: " + Формат(Длительность, "ЧГ=0; ЧН=0"));

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

Подробную картину по строкам кода даёт замер производительности в отладчике Конфигуратора или EDT. Он показывает, какая строка цикла съела больше всего времени.

Оптимизация: индексы, соответствия и замена циклов методами таблицы

Индекс по колонке для поиска

Если внутри цикла вы ищете строки в другой таблице по одной и той же колонке, создайте индекс:

Остатки.Индексы.Добавить("Номенклатура");

Для Каждого Строка Из ТаблицаТоваров Цикл
    Отбор = Новый Структура("Номенклатура", Строка.Номенклатура);
    Найденные = Остатки.НайтиСтроки(Отбор);
    Если Найденные.Количество() > 0 Тогда
        Строка.Остаток = Найденные[0].Количество;
    КонецЕсли;
КонецЦикла;

С индексом НайтиСтроки() не просматривает всю таблицу, а сразу находит строки по ключу. На таблицах в тысячи строк разница заметна сразу. Создавайте индекс один раз до цикла, а не внутри.

Соответствие вместо поиска

Когда ключ уникален, ещё проще заранее разложить таблицу в Соответствие:

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

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

Два линейных прохода вместо поиска на каждой итерации.

Методы таблицы, которые заменяют цикл

Часть задач вообще не требует ручного перебора. Платформа делает это сама и быстрее:

  • 📊 Таблица.Итог("Сумма") — сумма по колонке;
  • 📊 Таблица.Свернуть("Номенклатура", "Количество, Сумма") — группировка с суммированием;
  • 📊 Таблица.ВыгрузитьКолонку("Номенклатура") — массив значений колонки;
  • 📊 Таблица.ЗаполнитьЗначения(0, "Скидка") — одно значение во все строки колонки;
  • 📊 Таблица.Сортировать("Дата Убыв") — сортировка без самописного алгоритма;
  • 📊 Таблица.Скопировать(, "Номенклатура, Количество") — новая таблица только с нужными колонками.

Если таблица пришла из запроса, многие расчёты выгоднее перенести в сам запрос: группировку, отбор, соединение с остатками. Тогда цикл остаётся только для того, что запросом не сделать.

Проверьте себя
Таблица из 10 строк. Удаляем строки в цикле Для Каждого. Что произойдёт?
С какого числа начинается индекс первой строки таблицы значений?
Что вернёт Таблица.Количество() - 1 для пустой таблицы?
Какой способ быстрее найдёт строку по товару внутри цикла на 5 000 строк?

Где чаще всего ошибаются при обходе таблицы

  • ⚠️ Граница По Таблица.Количество() без минус единицы. На последней итерации индекс выйдет за пределы, и цикл упадёт с ошибкой Индекс находится за границами диапазона.
  • ⚠️ Счёт от единицы. Код Для Индекс = 1 По Таблица.Количество() Цикл Строка = Таблица[Индекс] пропустит первую строку и упадёт на последней.
  • ⚠️ Опечатка в имени колонки через скобки. Строка["Колличество"] не проверяется при сохранении модуля, ошибка Поле объекта не обнаружено всплывёт только на выполнении.
  • ⚠️ Перебор табличной части формы на клиенте с вызовом сервера на каждой строке. Каждый вызов — отдельный обмен с сервером. Соберите данные и сделайте один серверный вызов.
  • ⚠️ Изменение строки-копии. Если вы взяли строки через Скопировать(), правки в новой таблице не попадут в исходную. Это разные объекты.

Частые вопросы

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