Я обожаю 1С. Программировать в данной среде мне очень комфортно, а главное - разработка занимает относительно немного времени. В своё время, я даже написал статью на Хабре, в которой хвалил 1С и высказывал некоторые претензии.
Данной статьёй я планирую начать цикл "придирок" к языку, платформе, типовым решениям 1С, в целом, высказаться на тему - чего ещё не хватает.
Я искренне верю, что внутри фирмы "1С" наши письма с просьбами читают и анализируют. А также, полагаю, они просматривают публикации на Инфостарте.
В языке 1С платформы "1С:Предприятие 8" на данный момент существует три вида цикла:
- Цикл "Для" по счетчику, от меньшего к большему, с неизменным шагом +1;
- Цикл "Для каждого" для коллекций, поддерживающих перебор (большинство коллекций);
- Цикл "Пока" по условию (только в начале цикла).
И для решения основных задач этого достаточно. А какие виды циклов существуют ещё?
- Цикл "Для" по счетчику, с настраиваемым шагом (см. язык BASIC)
- Цикл "Для" от большего к меньшему (с отрицательным шагом -1, или настраиваемым, см. язык PASCAL)
- Цикл "Делай ... Пока" по условию (в конце цикла, так что цикл выполнится не менее 1 раза, см. язык PASCAL)
- Существуют также более сложные варианты циклов (см. язык С и все его "потомки").
Однажды, написав статью, расписывающую достоинства 1С, я упомянул среди недостатков - "мне не хватает обратного цикла". При этом, столкнулся с непониманием - "а зачем вообще нужен обратный цикл?". Сегодня я отвечу на этот вопрос.
Итак...
Рассмотрим задачу:
Есть таблица значений с колонкой, в которой хранятся числа. Требуется удалить все строки, в которых в указанной колонке хранятся четные значения.
Для моделирования такой таблицы и проверки результата подойдёт следующий код:
ТЗ = новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("Номер", новый ОписаниеТипов("Число"));
Массив = СтрРазделить("1, 1, 2, 1, 4, 6, 1, 1, 1, 8, 10, 1", ", ", Ложь);
Для каждого НомерСтрокой из Массив Цикл
ТЗ.Добавить().Номер = Число(НомерСтрокой);
КонецЦикла;
Сообщить("Исходник: " + СтрСоединить(ТЗ.ВыгрузитьКолонку("Номер"), ", "));
// место вставки обработчика
Сообщить("Результат: " + СтрСоединить(ТЗ.ВыгрузитьКолонку("Номер"), ", "));
Имя переменной таблицы "ТЗ", имя колонки "Номер";
Попробуем решить данную задачу "в лоб":
Для сч=0 По ТЗ.Количество()-1 Цикл
Если ТЗ[сч].Номер % 2 = 0 Тогда
ТЗ.Удалить(сч);
КонецЕсли;
КонецЦикла;
Попытка выполнить данный код приведет к исключению: "Индекс находится за границами массива" (да, да, именно "массива" - такую ошибку выдаёт платформа).
Причина ошибки в том, что выражение "ТЗ.Количество()-1" рассчитывается только один раз до начала выполнения цикла, а не на каждой итерации (это задействует меньше вычислений и оптимально для большинства случаев использования "Для").
А, т.к. внутри цикла мы удаляем элементы коллекции, то размер этой коллекции тоже уменьшается. В итоге, счетчик выходит за пределы нашей коллекции и попытка чтения элемента приводит к ошибке.
Попробуем реализовать это через цикл "Пока"
сч = 0;
Пока сч<ТЗ.Количество() Цикл
Если ТЗ[сч].Номер % 2 = 0 Тогда
ТЗ.Удалить(сч);
КонецЕсли;
сч = сч + 1;
КонецЦикла;
Теперь мы проверяем Количество() при каждой итерации, что неоптимально в плане вычислительных ресурсов, но позволяет избежать выхода за границы. Вместо этого, можно присвоить ТЗ.Количество() некоторой переменной, сверять сч с ней, и уменьшать её когда удаляем строку. Это будет оптимальнее, но на несколько строк кода больше (а чем больше строк, тем сложнее потом будет его читать и разбирать).
Этот код уже выполнится.
Но в результате выдаст:
Исходник: 1, 1, 2, 1, 4, 6, 1, 1, 1, 8, 10, 1
Результат: 1, 1, 1, 6, 1, 1, 1, 10, 1
Почему удалились не все чётные числа? Давайте разберем пошагово:
сч = 0: Массив значений в колонке: [1, 1, 2, 1, 4, 6, 1, 1, 1, 8, 10, 1], значение в ячейке = 1, пропускаем
сч = 1: Массив значений в колонке: [1, 1, 2, 1, 4, 6, 1, 1, 1, 8, 10, 1], значение в ячейке = 1, пропускаем
сч = 2: Массив значений в колонке: [1, 1, 2, 1, 4, 6, 1, 1, 1, 8, 10, 1], значение в ячейке = 2, удаляем, массив сместился на 1 влево
сч = 3: Массив значений в колонке: [1, 1, 1, 4, 6, 1, 1, 1, 8, 10, 1], значение в ячейке = 4, удаляем, массив сместился на 1 влево
сч = 4: Массив значений в колонке: [1, 1, 1, 6, 1, 1, 1, 8, 10, 1], значение в ячейке = 1, пропускаем (!)
сч = 5: Массив значений в колонке: [1, 1, 1, 6, 1, 1, 1, 8, 10, 1], значение в ячейке = 1, пропускаем
сч = 6: Массив значений в колонке: [1, 1, 1, 6, 1, 1, 1, 8, 10, 1], значение в ячейке = 1, пропускаем
сч = 7: Массив значений в колонке: [1, 1, 1, 6, 1, 1, 1, 8, 10, 1], значение в ячейке = 8, удаляем, массив сместился на 1 влево
сч = 8: Массив значений в колонке: [1, 1, 1, 6, 1, 1, 1, 10, 1], значение в ячейке = 1, пропускаем
сч = 9, сч=ТЗ.Количество(), цикл завершен
(красным выделены значения в строке [сч])
Обратите внимание, когда рядом стоят два четных числа, то после удаления строки - второе число смещается на 1 влево, при этом, счетчик увеличивается на 1 и, получается, перескакивает это число.
Такое происходит не только когда два четных числа стоят рядом. Обратите внимание, что когда сч=2 тоже происходит "перескакивание" через 1 значение, но т.к. оно является нечетным и всё равно не должно было быть удалено, то в результате выполнения это незаметно.
А как себя поведёт цикл "Для каждого"?
Для Каждого Строка из ТЗ Цикл
Если Строка.Номер % 2 = 0 Тогда
ТЗ.Удалить(Строка);
КонецЕсли;
КонецЦикла;
Этот код уже выполнится без ошибок. Но результат выдаст точно такой-же, как и цикл "Пока"
Исходник: 1, 1, 2, 1, 4, 6, 1, 1, 1, 8, 10, 1
Результат: 1, 1, 1, 6, 1, 1, 1, 10, 1
Как бы решалась эта задача, если бы у нас был обратный цикл?
"Для" со счетчиком -1 решил бы эту проблему:
Для ТЗ.Количество()-1 По сч=0 Цикл //обратный или шаг -1
Если ТЗ[сч].Номер % 2 = 0 Тогда
ТЗ.Удалить(сч);
КонецЕсли;
КонецЦикла;
Т.к. обход коллекции выполняется в обратном порядке, удаление элемента смещает массив относительно счетчика цикла, но и счетчик смещается в том-же направлении.
Так как-же быть?
А теперь, варианты костылей, с помощью которых решается эта задача (все они приводят к желаемому результату):
1. Аналог решения, встречаемый в типовых конфигурациях 1С:
МассивНаУдаление = новый Массив;
Для каждого Строка из ТЗ Цикл
Если Строка.Номер % 2 = 0 Тогда
МассивНаУдаление.Добавить(Строка);
КонецЕсли;
КонецЦикла;
Для каждого Строка из МассивНаУдаление Цикл
ТЗ.Удалить(Строка);
КонецЦикла;
Нужны комментарии?
2. Вариант с циклом "Пока", но со смещением или счетчика или строки
сч = 0;
Пока сч<ТЗ.Количество() Цикл
Если ТЗ[сч].Номер % 2 = 0 Тогда
ТЗ.Удалить(сч);
Иначе
сч = сч + 1;
КонецЕсли;
КонецЦикла;
3. Еще один вариант с циклом "Пока"
сч = 0;
Пока сч<ТЗ.Количество() Цикл
Если ТЗ[сч].Номер %2 = 0 Тогда
ТЗ.Удалить(сч);
сч = сч - 1;
КонецЕсли;
сч = сч + 1;
КонецЦикла;
4. Эмуляция обратного цикла с помощью цикла "Пока"
(сам я, чаще всего, в коде делаю именно так, т.к. это минимум лишних операций и уменьшение счетчика в первой-же строке внутри цикла гарантирует, что если в теле цикла будет "Продолжить" - не произойдёт зацикливание)
сч = ТЗ.Количество();
Пока сч>0 Цикл
сч = сч - 1;
Если ТЗ[сч].Номер % 2 = 0 Тогда
ТЗ.Удалить(сч);
КонецЕсли;
КонецЦикла;
5. Эмуляция обратного цикла с помощью цикла "Для"
(а этот метод мне попался недавно, и заинтересовал. Хотя количество операций смены знака здесь немалое, код смотрится весьма лаконично)
Для сч = -ТЗ.Количество() + 1 По 0 Цикл
Если ТЗ[-сч].Номер % 2 = 0 Тогда
ТЗ.Удалить(-сч);
КонецЕсли;
КонецЦикла;
Ну да, цикл "Для" хоть и по счетчику +1, но нормально работает с отрицательными числами.
6. Еще такой вариант встречается часто:
Количество = Коллекция.Количество();
Для Номер = 1 По Количество Цикл
ОбратныйИндекс = Количество - Номер;
Коллекция.Удалить(ОбратныйИндекс);
КонецЦикла;
Автор: (70) PerlAmutor 29.05.23 06:49
Это упрощенная вариация задач, периодически встречающихся в разработке, к задачам такого-же плана относятся задачи вида "удалить все цифры из строки", "удалить дублирующиеся элементы в коллекции". Даже сама фирма 1С сталкивается с подобными задачами при разработке своих типовых решений. И задача очень лаконично решается через обратный цикл. Вот почему мне бы хотелось видеть его в составе конструкций языка 1С (а заодно, можно бы и "Для каждого" допилить, чтобы не пропускал элементы коллекции).