Перебор элементов СписокЗначений во встроенном языке 1С 8.3: удаление на ходу, вложенность и скорость

· 9 мин

Содержание

Обойти список значений в 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"), Строка(Элемент.Значение)));
		КонецЕсли;
	КонецЦикла;
КонецПроцедуры

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

Чем отличается обход соседних коллекций

Внутри списка часто лежат другие коллекции, и переменная цикла у них получает разное:

КоллекцияЧто получает переменная Для КаждогоКак достать данные
СписокЗначенийЭлементСпискаЗначений.Значение, .Представление, .Пометка
МассивСамо значениеСразу переменная
СтруктураКлючИЗначение.Ключ, .Значение
СоответствиеКлючИЗначение.Ключ, .Значение
ТаблицаЗначенийСтрокаТаблицыЗначенийСтрока.ИмяКолонки

Универсальная рекурсия проверяет тип через ТипЗнч и для каждой коллекции вызывает свою ветку. Для структуры внутри списка это выглядит так:

Если ТипЗнч(Элемент.Значение) = Тип("Структура") Тогда
	Для Каждого КлючИЗначение Из Элемент.Значение Цикл
		Сообщить(КлючИЗначение.Ключ + " = " + Строка(КлючИЗначение.Значение));
	КонецЦикла;
КонецЕсли;

Если вложенность фиксированная (например, «отдел → список сотрудников»), рекурсия не нужна. Два вложенных цикла «Для Каждого» читаются проще.

Ошибки при переборе списка значений

Каждая из них встречается в реальном коде и даёт либо сообщение платформы, либо тихо неверный результат.

  1. Поле объекта не обнаружено на строке вида Элемент.Наименование. У ЭлементСпискаЗначений нет реквизитов справочника. Нужно Элемент.Значение.Наименование — а лучше запрос, как показано выше.
  2. Сумма или сравнение с самим элементом. Если Элемент = 100 всегда ложно: элемент — объект, а не число. Сравнивайте Элемент.Значение.
  3. Граница Количество() вместо Количество() - 1. Индексы с нуля, поэтому последний шаг выходит за конец списка и даёт Индекс находится за границами диапазона.
  4. Склейка числа со строкой без формата. "Сумма: " + 12500 выведет «12 500» с неразрывным пробелом, который ломает выгрузки и сравнения строк. Используйте Формат(Число, "ЧГ=0").
  5. Смешение значения и представления. Поиск НайтиПоЗначению("Аванс") в нашем примере ничего не найдёт: «Аванс» — это представление, а значение — число 100. Ищите по значению или переберите список и сравните Элемент.Представление.
  6. Неучтённые разные типы значений. В одном списке могут лежать числа и Неопределено. Перед арифметикой проверьте ТипЗнч(Элемент.Значение) = Тип("Число").

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

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