При написании алгоритмов на встроенном языке 1С начинающие разработчики часто сталкиваются с неочевидным поведением циклов и условных операторов. Одна из классических ошибок — неверное позиционирование оператора Прервать внутри вложенных конструкций. В этой статье мы подробно разберем, почему смещение одной строки кода может кардинально изменить результат работы программы, а также рассмотрим эффективные методы отладки и оптимизации алгоритмов поиска простых чисел.
Рассмотрим задачу, которую пытался решить автор темы: вывести все простые числа в диапазоне от 2 до 1000. Напомним, что простым называется натуральное число, которое имеет ровно два различных натуральных делителя — единицу и самого себя.
В первом (некорректном) варианте код выглядел следующим образом:
Для НатуральноеЧисло = 2 По 1000 Цикл
ЧислоЯвляетсяПростым = Истина;
Для Делитель = 2 По (НатуральноеЧисло - 1) Цикл
Если НатуральноеЧисло % Делитель = 0 Тогда
ЧислоЯвляетсяПростым = Ложь;
КонецЕсли;
Прервать; // Оператор находится ВНЕ конструкции Если, но внутри цикла по делителям
КонецЦикла;
Если ЧислоЯвляетсяПростым = Истина Тогда
Сообщить(НатуральноеЧисло);
КонецЕсли;
КонецЦикла;
При запуске этого кода программа выдает некорректный результат: выводятся число 2 и все нечетные числа (3, 5, 7, 9, 11, 13, 15...). Числа 9, 15, 21 не являются простыми, однако алгоритм посчитал их таковыми. Давайте разберем по шагам, почему так произошло.
Проанализируем ситуацию, когда управление передается во внутренний цикл Для Делитель = 2 По ....
Основная проблема заключается в том, где именно написан оператор Прервать. Он расположен сразу после закрытия условного оператора КонецЕсли, но все еще принадлежит телу внутреннего цикла. Это означает следующее:
НатуральноеЧисло (например, 9) и заходит во внутренний цикл.Делитель принимает свое начальное значение — 2.9 % 2 = 0 (остаток от деления 9 на 2 равен 1). Условие ложно, поэтому блок внутри Если игнорируется, и переменная ЧислоЯвляетсяПростым остается равной Истина.Если ... КонецЕсли и сразу же натыкается на инструкцию Прервать;.Прервать выполняется безусловно. Платформе не важно, заходили мы в ветку Если или нет. Команда Прервать незамедлительно останавливает работу текущего цикла по делителям.Делитель = 2). Проверка на другие делители (3, 4, 5...) даже не начинается!ЧислоЯвляетсяПростым остался равен Истина. Программа ошибочно выводит число 9 на экран как простое.Таким образом, в дефектном коде проверяется делимость чисел только на двойку. Все четные числа отсеиваются (их флаг сбрасывается в Ложь), а все нечетные числа признаются простыми, так как они не делятся на 2 без остатка.
Чтобы программа работала корректно, оператор Прервать должен выполняться только тогда, когда делитель действительно найден. То есть команда прерывания должна находиться строго внутри условного блока Если ... КонецЕсли:
Для НатуральноеЧисло = 2 По 1000 Цикл
ЧислоЯвляетсяПростым = Истина;
Для Делитель = 2 По (НатуральноеЧисло - 1) Цикл
Если НатуральноеЧисло % Делитель = 0 Тогда
ЧислоЯвляетсяПростым = Ложь;
Прервать; // Теперь прерывание сработает ТОЛЬКО при нахождении делителя
КонецЕсли;
КонецЦикла;
Если ЧислоЯвляетсяПростым Тогда
Сообщить(НатуральноеЧисло);
КонецЕсли;
КонецЦикла;
Теперь логика работает безупречно: если число разделилось без остатка, мы фиксируем, что оно составное (ЧислоЯвляетсяПростым = Ложь), и сразу же прекращаем дальнейший перебор делителей с помощью Прервать, так как продолжать поиск больше нет смысла. Если же число не делится на текущий Делитель, блок Если игнорируется, оператор Прервать не вызывается, и цикл переходит к следующему шагу (проверяет деление на 3, 4 и так далее).
Для эффективного программирования во встроенном языке 1С необходимо четко понимать разницу между основными операторами управления переходами в циклах:
Прервать (Break): полностью прекращает выполнение текущего цикла. Управление передается первой строке кода, следующей сразу за ключевым словом КонецЦикла.Продолжить (Continue): завершает выполнение текущей итерации цикла. Платформа пропускает весь код, написанный ниже этой команды до конца тела цикла, и сразу переходит к следующему шагу (выполняет инкремент счетчика в цикле Для или проверку условия в цикле Пока).Важное правило вложенности: операторы Прервать и Продолжить влияют только на тот цикл, в теле которого они непосредственно объявлены. Если у вас используются вложенные циклы, вызов Прервать во внутреннем цикле никак не остановит внешний цикл.
Рассмотрим пример, когда необходимо выйти сразу из двух циклов при определенных условиях. В 1С нет оператора «выхода из всех вложенных циклов» (аналога goto или именованных циклов), поэтому разработчики используют специальный флаг:
ВыйтиИзВсехЦиклов = Ложь;
Для СтрокаТаблицы = 1 По 100 Цикл
Для КолонкаТаблицы = 1 По 50 Цикл
Если ПроверитьЗначение(СтрокаТаблицы, КолонкаТаблицы) Тогда
ВыйтиИзВсехЦиклов = Истина;
Прервать; // Выходим из внутреннего цикла
КонецЕсли;
КонецЦикла;
Если ВыйтиИзВсехЦиклов Тогда
Прервать; // Выходим из внешнего цикла
КонецЕсли;
КонецЦикла;
Самый лучший способ понять, как движутся потоки управления в программе, — запустить встроенный отладчик 1С. Разберем по шагам, как локализовать ошибку с оператором Прервать самостоятельно:
ЧислоЯвляетсяПростым = Истина; внутри внешнего цикла.Делитель, НатуральноеЧисло и выражение НатуральноеЧисло % Делитель. На каждом шаге вы будете видеть актуальные значения этих величин.Прервать; и выбрасывает вас из цикла по делителям, даже если число не делилось на двойку.Наш базовый рабочий алгоритм решает поставленную задачу, но делает это крайне медленно. Если увеличить лимит с 1000 до 100 000, программа начнет заметно зависать. Давайте разберем, как оптимизировать код, используя математические законы и особенности платформы 1С.
В текущем коде мы проверяем делители до значения НатуральноеЧисло - 1. Однако математически доказано, что если у числа $N$ есть составной делитель, то как минимум один из его делителей обязательно лежит в диапазоне от 2 до квадратного корня из этого числа ($\sqrt{N}$).
Например, для проверки числа 100 нам не нужно делить его на числа от 11 до 99. Если делителей нет в промежутке до 10 ($\sqrt{100}$), то их не будет и дальше. Для реализации этого правила в 1С можно использовать функцию Sqrt(), которая появилась в современной платформе, либо классическое возведение во вторую степень:
ГраницаДелителей = Цел(Sqrt(НатуральноеЧисло)); // Используем корень
// Или обходной путь через умножение во внутреннем цикле:
// Пока Делитель * Делитель <= НатуральноеЧисло Цикл
Единственным четным простым числом является 2. Все остальные четные числа гарантированно делятся на 2, поэтому проверять их не имеет смысла. Мы можем начать перебор внешнего цикла с 3 и идти с шагом 2 (только по нечетным числам).
Любое составное число можно разложить на простые множители. Это значит, что для проверки "простоты" числа его нужно делить не на все подряд числа, а только на ранее найденные простые числа, которые меньше или равны квадратному корню из проверяемого числа. Для этого мы можем сохранять найденные простые числа в массив.
Объединим все эти оптимизации в единый высокопроизводительный код на 1С:
Лимит = 1000;
МассивПростыхЧисел = Новый Массив;
МассивПростыхЧисел.Добавить(2); // Сразу добавим двойку
НатуральноеЧисло = 3;
Пока НатуральноеЧисло <= Лимит Цикл
ЧислоЯвляетсяПростым = Истина;
ПредельныйДелитель = Sqrt(НатуральноеЧисло);
Для Каждого РанееНайденноеПростое Из МассивПростыхЧисел Цикл
// Проверяем только делители, не превышающие корень из числа
Если РанееНайденноеПростое > ПредельныйДелитель Тогда
Прервать;
КонецЕсли;
Если НатуральноеЧисло % РанееНайденноеПростое = 0 Тогда
ЧислоЯвляетсяПростым = Ложь;
Прервать; // Нашли делитель - прерываем проверку
КонецЕсли;
КонецЦикла;
Если ЧислоЯвляетсяПростым Тогда
МассивПростыхЧисел.Добавить(НатуральноеЧисло);
КонецЕсли;
НатуральноеЧисло = НатуральноеЧисло + 2; // Переходим к следующему нечетному
КонецЦикла;
// Выводим результат в синтаксисе 1С
Для Каждого ПростыеЧисла Из МассивПростыхЧисел Цикл
Сообщить(ПростыеЧисла);
КонецЦикла;
Этот оптимизированный алгоритм работает в десятки раз быстрее первоначальной версии за счет резкого сокращения количества итераций во внутреннем цикле и отсутствия лишних проверок для четных чисел.