Перебор элементов СписокЗначений во встроенном языке 1С 8.3: удаление на ходу, вложенность и скорость
Содержание
Обойти список значений в 1С проще всего циклом Для Каждого Элемент Из Список Цикл … КонецЦикла. Переменная цикла получает не само значение, а ЭлементСпискаЗначений, поэтому данные берут через Элемент.Значение, подпись — через Элемент.Представление, флажок — через Элемент.Пометка. Если по ходу перебора элементы нужно удалять, используют обратный цикл по индексу от последнего элемента к первому.
Эти два способа закрывают почти все задачи. Остальное — эффективные методы для частных случаев: быстрый поиск через Соответствие, выгрузка в массив, рекурсия для вложенных списков и один запрос вместо обращений к реквизитам в цикле. Всё ниже относится к платформе 8.3 и одинаково работает в любой конфигурации: БП, ЗУП, УТ или самописной.
Из чего состоит список значений и что получает цикл
СписокЗначений — упорядоченная коллекция, где каждый элемент хранит четыре свойства:
| Свойство | Что хранит | Пример |
|---|---|---|
Значение | Сами данные любого типа | ссылка на контрагента, число, строка |
Представление | Текст для показа пользователю | «ООО Ромашка (основной)» |
Пометка | Булево, флажок | Истина у выбранных строк |
Картинка | Значок элемента | обычно не заполняют |
Отсюда главное правило обхода: в цикле вы работаете с элементом, а не с его значением. Код Сумма = Сумма + Элемент не сложит числа, а вызовет ошибку преобразования типа. Правильно — Сумма = Сумма + Элемент.Значение.
Индексы элементов начинаются с нуля. У списка из 10 элементов первый имеет индекс 0, последний — 9. Количество элементов возвращает метод Количество().
Подготовим тестовый список, на котором дальше разберём все способы:
Список = Новый СписокЗначений;
Список.Добавить(100, "Аванс");
Список.Добавить(250, "Зарплата", Истина);
Список.Добавить(0, "Пустая строка");
Список.Добавить(75, "Премия", Истина);
Метод Добавить принимает значение, представление и пометку. В списке четыре элемента с индексами от 0 до 3.
Обход списка значений циклом «Для Каждого»
Это основной и самый читаемый способ. Платформа сама переходит от элемента к элементу, счётчик не нужен.
Итого = 0;
Для Каждого Элемент Из Список Цикл
Итого = Итого + Элемент.Значение;
Сообщить(СтрШаблон("%1: %2", Элемент.Представление, Формат(Элемент.Значение, "ЧН=0; ЧГ=0")));
КонецЦикла;
// Итого = 425
Проверим арифметику: 100 + 250 + 0 + 75 = 425.
Что ещё полезно знать про этот цикл:
- 🔁
Продолжитьпереходит к следующему элементу, не выполняя остаток тела цикла. - ⛔
Прерватьвыходит из цикла совсем — удобно, когда нужный элемент уже найден. - ✏️ Свойства текущего элемента можно менять прямо в цикле:
Элемент.Пометка = ИстинаилиЭлемент.Представление = "…". Переменная ссылается на настоящий элемент списка, а не на копию. - 🚫 Добавлять и удалять элементы внутри «Для Каждого» нельзя — об этом отдельный раздел ниже.
Пример с отбором только помеченных строк:
Выбранные = Новый Массив;
Для Каждого Элемент Из Список Цикл
Если Не Элемент.Пометка Тогда
Продолжить;
КонецЕсли;
Выбранные.Добавить(Элемент.Значение);
КонецЦикла;
// Выбранные: 250, 75
Если нужно просто получить все значения массивом, цикл писать не обязательно: Массив = Список.ВыгрузитьЗначения();. Обратная операция — Список.ЗагрузитьЗначения(Массив), она заменяет содержимое списка.
Перебор списка значений по индексу
Цикл со счётчиком нужен, когда важен номер элемента: сравнить соседние строки, обработать каждый второй элемент, пройти список с конца.
Прямой обход
Для Индекс = 0 По Список.Количество() - 1 Цикл
Элемент = Список[Индекс];
Сообщить(СтрШаблон("№%1 — %2", Формат(Индекс + 1, "ЧГ=0"), Элемент.Представление));
КонецЦикла;
Вместо Список[Индекс] можно писать Список.Получить(Индекс) — результат тот же. Квадратные скобки короче и читаются привычнее.
Для пустого списка граница равна -1, и тело цикла не выполнится ни разу. Отдельная проверка на пустоту здесь не нужна.
Сравнение соседних элементов
Типичная задача — найти места, где значение резко выросло:
Для Индекс = 1 По Список.Количество() - 1 Цикл
Текущее = Список[Индекс].Значение;
Предыдущее = Список[Индекс - 1].Значение;
Если Текущее > Предыдущее * 2 Тогда
Сообщить("Скачок на элементе " + Список[Индекс].Представление);
КонецЕсли;
КонецЦикла;
Цикл начинается с индекса 1, иначе на первом шаге Индекс - 1 даст -1 и ошибку. На нашем списке сработает элемент «Зарплата»: 250 больше, чем 100 × 2.
Обратный обход
Всего = Список.Количество();
Для Сч = 1 По Всего Цикл
Элемент = Список[Всего - Сч];
// обработка: индексы идут 3, 2, 1, 0
КонецЦикла;
Количество сохраняется в переменную Всего один раз до цикла. Это важно, если в цикле будут удаления: индекс считается от неизменной величины, а не от текущей длины списка.
Цикл «Пока» для поиска до первого совпадения
Когда обход нужно остановить на первом подходящем элементе и при этом знать его номер, удобен цикл «Пока»:
Индекс = 0;
Найден = Неопределено;
Пока Индекс < Список.Количество() И Найден = Неопределено Цикл
Если Список[Индекс].Пометка Тогда
Найден = Список[Индекс];
КонецЕсли;
Индекс = Индекс + 1;
КонецЦикла;
Здесь условие проверяется на каждом шаге, поэтому Количество() всегда актуально. На нашем списке цикл остановится на элементе «Зарплата» с индексом 1.
Изменение и удаление элементов во время обхода
Менять Значение, Представление и Пометку можно в любом цикле. Проблемы начинаются, когда меняется состав списка.
Почему нельзя удалять в «Для Каждого»
Цикл «Для Каждого» внутри идёт по позициям. Вы удалили элемент с позиции 2 — следующий сдвинулся на его место, а цикл уже шагнул на позицию 3. В итоге один элемент пропускается без всякой ошибки. Такой код работает «почти правильно» и ломается только на подряд идущих элементах, которые нужно удалить.
Почему ломается прямой цикл по индексу
Граница цикла По Список.Количество() - 1 вычисляется один раз при входе. После удаления элементов список стал короче, а счётчик дойдёт до старой границы. Результат — ошибка Индекс находится за границами диапазона. Плюс тот же пропуск соседнего элемента.
Рабочий способ 1: обратный цикл
Удаляем с конца — сдвигаются только уже обработанные элементы:
Всего = Список.Количество();
Для Сч = 1 По Всего Цикл
Индекс = Всего - Сч;
Если Список[Индекс].Значение = 0 Тогда
Список.Удалить(Индекс);
КонецЕсли;
КонецЦикла;
Метод Удалить принимает либо индекс, либо сам элемент списка.
Рабочий способ 2: собрать, потом удалить
Когда условие сложное и обратный порядок неудобен, сначала соберите элементы в массив, а удаляйте после цикла:
КУдалению = Новый Массив;
Для Каждого Элемент Из Список Цикл
Если Элемент.Значение = 0 Тогда
КУдалению.Добавить(Элемент);
КонецЕсли;
КонецЦикла;
Для Каждого Элемент Из КУдалению Цикл
Список.Удалить(Элемент);
КонецЦикла;
В массив кладутся сами элементы, а не индексы: индексы после первого удаления уже неверны.
Добавление по ходу перебора
Новые элементы тоже лучше копить отдельно и добавлять после цикла. Если добавлять в конец обходимого списка, цикл может дойти до новых элементов и обработать их, а при неудачном условии — не закончиться вовсе.
Метод Скопировать() создаёт новый список с новыми элементами. Удалить из исходного списка элемент копии не получится — нужно искать его через НайтиПоЗначению. Поэтому «обойти копию и чистить оригинал» — неудачный приём.
Сравнение методов обхода и производительность
На списках в сотни и тысячи элементов разница между способами перебора незаметна. Тормозит не сам цикл, а то, что делается внутри него.
| Способ | Когда брать | Риски |
|---|---|---|
Для Каждого | Чтение и правка свойств элементов | Нельзя менять состав списка |
| Прямой цикл по индексу | Нужен номер, соседние элементы | Ошибка границ при удалении |
| Обратный цикл | Удаление по условию | Порядок обработки с конца |
ВыгрузитьЗначения() + массив | Нужны только значения, передача в другие методы | Копия занимает память |
| Сбор в массив, удаление после | Сложное условие отбора | Лишний проход по списку |
Что реально замедляет обход
- 🐢 Поиск внутри цикла.
НайтиПоЗначениюпросматривает список с начала. Если вызывать его в цикле по другому списку, число операций растёт как произведение размеров: 5 000 × 5 000 — это 25 миллионов сравнений. - 🐢 Обращение к реквизитам ссылок через точку. Строка
Элемент.Значение.ИННдля ссылки на контрагента читает данные из базы. В цикле на 3 000 элементов это тысячи отдельных обращений. - 🐢 Вызовы сервера из клиентского цикла. Каждый вызов серверной процедуры внутри клиентского цикла — отдельный сетевой обмен. На клиенте реквизиты ссылок через точку вообще недоступны, их получают на сервере.
Как ускорить
Для поиска заведите Соответствие один раз до цикла. Поиск по ключу в нём не зависит от размера коллекции:
Индекс = Новый Соответствие;
Для Каждого Элемент Из Список Цикл
Индекс.Вставить(Элемент.Значение, Элемент);
КонецЦикла;
НайденныйЭлемент = Индекс.Получить(250); // Неопределено, если ключа нет
Для данных из базы передайте весь список в запрос параметром. Условие В (&Список) принимает и список значений, и массив:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Ссылка,
| Контрагенты.ИНН
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Ссылка В (&Список)";
Запрос.УстановитьПараметр("Список", СписокКонтрагентов);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Строка(Выборка.Ссылка) + " — " + Выборка.ИНН);
КонецЦикла;
Один запрос вместо тысяч обращений через точку — самый заметный выигрыш, который можно получить на обходе.
Как замерить самому
Не верить общим словам о скорости, а измерить на своих данных помогает функция ТекущаяУниверсальнаяДатаВМиллисекундах():
Начало = ТекущаяУниверсальнаяДатаВМиллисекундах();
Для Каждого Элемент Из БольшойСписок Цикл
Х = Элемент.Значение;
КонецЦикла;
Длительность = ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало;
Сообщить(СтрШаблон("Для Каждого: %1 мс", Формат(Длительность, "ЧН=0; ЧГ=0")));
Запустите замер несколько раз подряд: первый прогон часто медленнее из-за прогрева. Для точного профилирования есть замер производительности в отладчике Конфигуратора или EDT.
Обход вложенных списков и других коллекций
Значение элемента может быть другим списком значений, массивом или структурой. Для дерева произвольной глубины пишут рекурсивную процедуру.
Процедура ОбойтиСписок(Список, Уровень = 0)
Если Уровень > 20 Тогда
ВызватьИсключение "Слишком глубокая вложенность списка";
КонецЕсли;
Для Каждого Элемент Из Список Цикл
Если ТипЗнч(Элемент.Значение) = Тип("СписокЗначений") Тогда
ОбойтиСписок(Элемент.Значение, Уровень + 1);
Иначе
Сообщить(СтрШаблон("Уровень %1: %2", Формат(Уровень, "ЧН=0; ЧГ=0"), Строка(Элемент.Значение)));
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Ограничение глубины защищает от зацикливания: если список по ошибке содержит сам себя, без проверки процедура уйдёт в бесконечную рекурсию.
Чем отличается обход соседних коллекций
Внутри списка часто лежат другие коллекции, и переменная цикла у них получает разное:
| Коллекция | Что получает переменная Для Каждого | Как достать данные |
|---|---|---|
СписокЗначений | ЭлементСпискаЗначений | .Значение, .Представление, .Пометка |
Массив | Само значение | Сразу переменная |
Структура | КлючИЗначение | .Ключ, .Значение |
Соответствие | КлючИЗначение | .Ключ, .Значение |
ТаблицаЗначений | СтрокаТаблицыЗначений | Строка.ИмяКолонки |
Универсальная рекурсия проверяет тип через ТипЗнч и для каждой коллекции вызывает свою ветку. Для структуры внутри списка это выглядит так:
Если ТипЗнч(Элемент.Значение) = Тип("Структура") Тогда
Для Каждого КлючИЗначение Из Элемент.Значение Цикл
Сообщить(КлючИЗначение.Ключ + " = " + Строка(КлючИЗначение.Значение));
КонецЦикла;
КонецЕсли;
Если вложенность фиксированная (например, «отдел → список сотрудников»), рекурсия не нужна. Два вложенных цикла «Для Каждого» читаются проще.
Ошибки при переборе списка значений
Каждая из них встречается в реальном коде и даёт либо сообщение платформы, либо тихо неверный результат.
Поле объекта не обнаруженона строке видаЭлемент.Наименование. УЭлементСпискаЗначенийнет реквизитов справочника. НужноЭлемент.Значение.Наименование— а лучше запрос, как показано выше.- Сумма или сравнение с самим элементом.
Если Элемент = 100всегда ложно: элемент — объект, а не число. СравнивайтеЭлемент.Значение. - Граница
Количество()вместоКоличество() - 1. Индексы с нуля, поэтому последний шаг выходит за конец списка и даётИндекс находится за границами диапазона. - Склейка числа со строкой без формата.
"Сумма: " + 12500выведет «12 500» с неразрывным пробелом, который ломает выгрузки и сравнения строк. ИспользуйтеФормат(Число, "ЧГ=0"). - Смешение значения и представления. Поиск
НайтиПоЗначению("Аванс")в нашем примере ничего не найдёт: «Аванс» — это представление, а значение — число 100. Ищите по значению или переберите список и сравнитеЭлемент.Представление. - Неучтённые разные типы значений. В одном списке могут лежать числа и
Неопределено. Перед арифметикой проверьтеТипЗнч(Элемент.Значение) = Тип("Число").
Частые вопросы
Можно ли получить элемент списка значений по представлению?
Элемент.Представление с нужной строкой, либо один раз постройте Соответствие «представление → элемент».Как узнать индекс элемента, найденного через НайтиПоЗначению?
Индекс(Элемент) у списка возвращает номер элемента с нуля. Это пригодится, если дальше нужен обход от найденной позиции.Как отсортировать список перед обходом?
СортироватьПоЗначению() и СортироватьПоПредставлению() меняют порядок в самом списке. Направление задаётся параметром НаправлениеСортировки.Убыв или НаправлениеСортировки.Возр.Как пометить все элементы без цикла?
ЗаполнитьПометки(Истина) ставит флажок всем элементам, ЗаполнитьПометки(Ложь) снимает со всех.Что быстрее передать на сервер: список значений или массив?
ВыгрузитьЗначения(). Он легче: в нём только сами значения.