Обход СписокЗначений в 1С 8.3: циклы, индексы, поиск и удаление элементов

· 10 мин

Содержание

Список значений в 1С 8.3 перебирают циклом Для Каждого Элемент Из Список Цикл ... КонецЦикла. На каждом шаге переменная Элемент содержит не само значение, а объект ЭлементСпискаЗначений, поэтому нужное значение берут через Элемент.Значение, а подпись — через Элемент.Представление.

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

Из чего состоит СписокЗначений

СписокЗначений — упорядоченная коллекция. Каждый её элемент — отдельный объект типа ЭлементСпискаЗначений с четырьмя свойствами:

СвойствоЧто хранитПример
ЗначениеСамо значение любого типассылка на контрагента, число, строка
ПредставлениеТекст, который видит пользователь«ООО Ромашка (основной)»
ПометкаБулево, флажок у строкиИстина
КартинкаКартинка у строки в формезначок из библиотеки

Заполняют список методом Добавить(). Параметры идут в том же порядке, что в таблице:

Список = Новый СписокЗначений;
Список.Добавить(100, "Сто");
Список.Добавить(250, "Двести пятьдесят", Истина);
Список.Добавить(40);  // представление пустое, в форме покажется само значение

Ещё несколько методов, которые понадобятся при обходе:

  • 🔢 Количество() — число элементов;
  • 📍 Получить(Индекс) или Список[Индекс] — элемент по номеру;
  • 🔎 Индекс(Элемент) — номер элемента в списке;
  • ➕ Вставить(Индекс, Значение, Представление) — вставка в нужную позицию;
  • ➖ Удалить(Индекс) или Удалить(Элемент) — удаление;
  • 📦 ВыгрузитьЗначения() и ЗагрузитьЗначения(Массив) — обмен с массивом.

У самого списка есть свойство ТипЗначения (описание типов). Если его задать, список проверяет тип добавляемых значений. В коде его обычно не трогают, а в формах им ограничивают ввод.

Перебор списка значений циклом Для Каждого

Это основной и самый читаемый способ. Платформа сама идёт от первого элемента к последнему, счётчик заводить не нужно.

Итого = 0;
Для Каждого Элемент Из Список Цикл
    Итого = Итого + Элемент.Значение;
    Сообщить(Элемент.Представление + ": " + Формат(Элемент.Значение, "ЧГ=0"));
КонецЦикла;
Сообщить("Итого: " + Формат(Итого, "ЧГ=0"));

Для списка из примера выше в Итого окажется 390 (100 + 250 + 40).

Внутри цикла работают два оператора управления:

  • ⏭️ Продолжить — пропустить текущий элемент и перейти к следующему;
  • ⛔ Прервать — выйти из цикла совсем, например когда нужный элемент уже найден.
Для Каждого Элемент Из Список Цикл
    Если Не Элемент.Пометка Тогда
        Продолжить;
    КонецЕсли;
    ОбработатьОтмеченный(Элемент.Значение);
КонецЦикла;

ЭлементСпискаЗначений — объект, а не копия. Если внутри цикла присвоить Элемент.Значение = Элемент.Значение * 2, изменится сам список. Это удобно для массовой правки: пометить все строки, поправить представления, пересчитать числа.

Для Каждого Элемент Из Список Цикл
    Элемент.Пометка = Истина;
    Элемент.Представление = "№ " + Формат(Список.Индекс(Элемент) + 1, "ЧГ=0") + " — " + Строка(Элемент.Значение);
КонецЦикла;

Для простой отметки всех строк есть готовый метод: Список.ЗаполнитьПометки(Истина). Цикл для этого писать не нужно.

Обход списка значений по индексу

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

Для Индекс = 0 По Список.Количество() - 1 Цикл
    Элемент = Список[Индекс];
    Сообщить(Формат(Индекс, "ЧН=0; ЧГ=0") + ". " + Строка(Элемент.Значение));
КонецЦикла;

Формат ЧН=0 нужен, чтобы ноль вывелся цифрой, а не пустой строкой.

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

Сравнение соседних элементов

Пример: найти, где числовой ряд перестаёт расти.

Для Индекс = 1 По Список.Количество() - 1 Цикл
    Текущее = Список[Индекс].Значение;
    Предыдущее = Список[Индекс - 1].Значение;
    Если Текущее < Предыдущее Тогда
        Сообщить("Спад на позиции " + Формат(Индекс, "ЧГ=0"));
    КонецЕсли;
КонецЦикла;

Цикл стартует с 1, чтобы у первого обрабатываемого элемента был предшественник с индексом 0.

Обратный цикл: удаление элементов при переборе

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

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

При Всего = 5 индекс пройдёт значения 4, 3, 2, 1, 0. Удаление элемента с индексом 3 не трогает позиции 0–2, которые ещё впереди, поэтому ничего не теряется.

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

Цикл Пока

Цикл Пока уместен, если число шагов заранее неизвестно: например, удаляете элементы, пока список не уложится в лимит.

Пока Список.Количество() > 10 Цикл
    Список.Удалить(Список.Количество() - 1);
КонецЦикла;

Здесь условие пересчитывается на каждом шаге — это и нужно, ведь список меняется.

Какой цикл быстрее и когда это важно

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

СпособКогда выбиратьРиски
Для Каждого ... ИзОбычный проход по всем элементамНет индекса; нельзя удалять в процессе
Для Индекс = 0 По ...Нужна позиция, соседи, шаг через одинОшибка на единицу, если начать с 1
Обратный Для Сч = 1 По ВсегоУдаление элементов по условиюНужно аккуратно считать индекс
Пока ... ЦиклЧисло шагов зависит от изменений спискаВечный цикл, если условие не меняется
ВыгрузитьЗначения() + массивНужны только значения, без представленийПометки и представления теряются

Где реально теряется время:

  • 🐢 вызов сервера на каждой итерации в клиентском коде — собирайте данные одним вызовом до цикла;
  • 🐢 запрос к базе внутри цикла по ссылкам — одна выборка с параметром-списком быстрее сотни мелких;
  • 🐢 НайтиПоЗначению() внутри цикла по другому большому списку — поиск линейный, общая работа растёт как произведение размеров;
  • 🐢 обращение через точку к реквизитам ссылки (Элемент.Значение.ИНН) — каждый такой доступ читает объект из базы.

Если сомневаетесь, замерьте. Для этого есть функция ТекущаяУниверсальнаяДатаВМиллисекундах():

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

Для точной картины используйте замер производительности в отладчике Конфигуратора или EDT: он показывает время по каждой строке кода.

Список значений можно передать параметром в запрос: ГДЕ Номенклатура.Ссылка В (&СписокТоваров). Платформа сама возьмёт значения элементов. Это быстрее, чем перебирать ссылки и обращаться к каждой отдельно.

Поиск и отбор элементов в списке значений

Поиск по значению

Метод НайтиПоЗначению() возвращает первый элемент с таким значением или Неопределено, если ничего не нашёл:

Найденный = Список.НайтиПоЗначению(250);
Если Найденный = Неопределено Тогда
    Сообщить("Значения 250 в списке нет");
Иначе
    Сообщить("Нашли на позиции " + Формат(Список.Индекс(Найденный), "ЧН=0; ЧГ=0"));
КонецЕсли;

Проверка на Неопределено обязательна. Без неё следующая строка с Найденный.Представление упадёт с ошибкой обращения к полю.

Поиск по представлению или условию

Готового поиска по представлению у списка нет. Его пишут циклом с Прервать:

Результат = Неопределено;
Для Каждого Элемент Из Список Цикл
    Если СтрНайти(НРег(Элемент.Представление), "двести") > 0 Тогда
        Результат = Элемент;
        Прервать;
    КонецЕсли;
КонецЦикла;

Фильтр: новый список по условию

Метода «отобрать» у списка значений тоже нет. Удобнее всего собрать новый список и не трогать исходный:

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

Для списка 100, 250, 40 в Отобранные попадут два элемента: 100 и 250.

Частый сценарий в формах — работать только с отмеченными строками. Пользователь ставит флажки, код берёт помеченные элементы тем же фильтром по Элемент.Пометка.

Много поисков подряд

Если нужно сотни раз проверять «есть ли значение в списке», один раз переложите значения в Соответствие и ищите по ключу:

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

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

Перебор вместе с сортировкой и выгрузкой в массив

Часто перед обходом список нужно упорядочить. Для этого есть два метода:

  • СортироватьПоЗначению(НаправлениеСортировки.Возр);
  • СортироватьПоПредставлению(НаправлениеСортировки.Убыв).

Оба меняют порядок в самом списке. Если исходный порядок ещё понадобится, сначала сделайте копию методом Скопировать() и сортируйте её.

Когда представления и пометки не нужны, проще работать с массивом:

Значения = Список.ВыгрузитьЗначения();
Для Каждого Значение Из Значения Цикл
    Сообщить(Значение);
КонецЦикла;

Здесь переменная цикла — уже само значение, без .Значение. Обратная операция — Список.ЗагрузитьЗначения(Массив): она очищает список и заполняет его элементами массива с пустыми представлениями.

Три типовые задачи на перебор списка значений

Собрать значения в строку через запятую

Нужно для сообщений и логов: «Выбраны: Москва, Казань, Пермь». Если значения — строки, хватит одной строки кода: СтрСоединить(Список.ВыгрузитьЗначения(), ", "). Для чисел и ссылок соберите массив представлений циклом:

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

Убрать повторяющиеся значения

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

Встречено = Новый Соответствие;
Всего = Список.Количество();
Для Сч = 1 По Всего Цикл
    Индекс = Всего - Сч;
    Значение = Список[Индекс].Значение;
    Если Встречено.Получить(Значение) <> Неопределено Тогда
        Список.Удалить(Индекс);
    Иначе
        Встречено.Вставить(Значение, Истина);
    КонецЕсли;
КонецЦикла;

Для ряда 5, 7, 5, 9, 7 останется 5, 9, 7: цикл идёт с конца, встречает 7 и 9, затем 5, а повторные 7 и 5 в начале удаляет.

Посчитать сумму только отмеченных строк

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

Сумма = 0;
Отмечено = 0;
Для Каждого Элемент Из Список Цикл
    Если Элемент.Пометка Тогда
        Сумма = Сумма + Элемент.Значение;
        Отмечено = Отмечено + 1;
    КонецЕсли;
КонецЦикла;

Если в списке 100 (отмечен), 250 (отмечен) и 40 (без отметки), получится Сумма = 350 и Отмечено = 2.

Ошибки при переборе списка значений и как их избежать

Диагностика: выберите симптом

Ещё две ошибки встречаются реже, но отнимают много времени:

  • ⚠️ Изменения «не видны» в форме. Если в форме список хранится в реквизите, а код меняет его копию, полученную через Скопировать(), исходный реквизит остаётся прежним. Меняйте сам реквизит формы.
  • ⚠️ Диалоговые методы на сервере. ПоказатьВыборЭлемента() и ПоказатьОтметкуЭлементов() работают только на клиенте. В серверной процедуре их вызвать нельзя, а сам перебор и поиск работают и там, и там.

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

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