Этот вопрос появился у нас в процессе обсуждения некоторых технических вопросов и проведения code-review, но мне интересно обсудить его и на данном тематическом ресурсе. Скажу сразу - мнение в нашей команде разделилось.
Со времен «старой школы» существует мнение, что структурное программирование — это хорошо, а любое отступление от него — это плохо. Однако, общение с профессиональными программистами показывает, что использование операторов break, continue, return на практике применяется довольно часто, потому что это удобно и в большинстве случаев делает программу более понятной. Если взять код типовой конфигурации от компании 1С и немного покопаться, то встречаются эти оба подхода вперемешку (предположу, что писали разные люди в разные временные промежутки).
И действительно Дональд Кнут в свое время писал:
«… Настоящая цель программиста состоит в том, чтобы формулировать наши программы таким образом, чтобы их было легко понимать.»
Давайте рассмотрим примеры использования и не использования этих операторов и сравним.
1. Оператор Перейти (GoTo).
Это совсем дурной тон и его использовать не будем). Однако интересно было бы услышать ваши мысли, возможно в сообществе еще есть «староверцы».
2. Оператор Возврат (Return).
Давайте рассмотрим некоторый виртуальный пример. Задача будет следующая: Требуется написать функцию получения математическое ожидание покупок клиента или средний чек.
Функция ПолучитьСреднийЧек(Партнер) Экспорт
// уходим если ошибка, обрабатывать не стоит
Если НЕ ЗначениеЗаполнено(Партнер) Тогда
Возврат новый Структура(Неопределено,"Данные пустые");
КонецЕсли;
Запрос = новый Запрос;
Запрос.Текст="ВЫБРАТЬ
| СРЕДНЕЕ(РасчетыСКлиентами.Сумма) КАК СреднийЧек
|ИЗ
| РегистрНакопления.РасчетыСКлиентами КАК РасчетыСКлиентами
|ГДЕ
| РасчетыСКлиентами.АналитикаУчетаПоПартнерам.Партнер = &Партнер
| И РасчетыСКлиентами.Регистратор ССЫЛКА Документ.РеализацияТоваровУслуг
| И РасчетыСКлиентами.Сумма <> 0
|
|ИМЕЮЩИЕ
| НЕ СРЕДНЕЕ(РасчетыСКлиентами.Сумма) ЕСТЬ NULL";
Запрос.УстановитьПараметр("Партнер",Партнер);
Результат = Запрос.Выполнить();
Если Результат.Пустой() Тогда
Возврат новый Структура(Неопределено,"Результат пустой");
КонецЕсли;
// На самом деле тут может быть многоа кода
// очень много кода
// более размера одного экрана
// ...
// ...
Выборка.Следующий();
Возврат новый Структура(Выборка.СреднийЧек,"Все успешно");
КонецФункции
Функция ПолучитьСреднийЧек(Данные) Экспорт
СтруктураВозврата = Неопределено;
// уходим если ошибка, обрабатывать не стоит
Если ЗначениеЗаполнено(Партнер) Тогда
Запрос = новый Запрос;
Запрос.Текст="ВЫБРАТЬ
| СРЕДНЕЕ(РасчетыСКлиентами.Сумма) КАК СреднийЧек
|ИЗ
| РегистрНакопления.РасчетыСКлиентами КАК РасчетыСКлиентами
|ГДЕ
| РасчетыСКлиентами.АналитикаУчетаПоПартнерам.Партнер = &Партнер
| И РасчетыСКлиентами.Регистратор ССЫЛКА Документ.РеализацияТоваровУслуг
| И РасчетыСКлиентами.Сумма <> 0
|
|ИМЕЮЩИЕ
| НЕ СРЕДНЕЕ(РасчетыСКлиентами.Сумма) ЕСТЬ NULL";
Запрос.УстановитьПараметр("Партнер",Партнер);
Результат = Запрос.Выполнить();
Если НЕ Результат.Пустой() Тогда
// На самом деле тут может быть многоа кода
// очень много кода
// более размера одного экрана
// ...
// ...
Выборка.Следующий();
СтруктураВозврата = новый Структура(Выборка.СреднийЧек,"Все успешно");
Иначе
СтруктураВозврата = новый Структура(Неопределено,"Результат пустой");
КонецЕсли;
Иначе
СтруктураВозврата = новый Структура(Неопределено,"Данные пустые");
КонецЕсли;
Возврат СтруктураВозврата;
КонецФункции
Какие выводы мы можем сделать из двух вышеуказанных примеров?
- Во втором примере мы теряем локальность — при достаточно длинном тексте процедуры нужно еще посмотреть, не выполняются ли какие-то действия после условного оператора. Да и не совсем понятно, зачем добавлять лишний уровень вложенности. Теперь представьте, что вы проводите анализ кода (code-review), то вам придется время от времени скролить вверх и вниз всю эту простыню кода, чтобы вернуться к оператору условий.
- Дополнительно во втором примере нам пришлось вводить переменную, в которой хранится результат выполненных операций. Вспоминаем о «Бритве Оккама» и понятии «Не плоди лишних сущностей».
- Как вы уже догадались, то проблему большого куска кода в обоих случаях мы можем подвергнуть (в принципе должны) рефакторингу и заключить в отдельную функцию.
- В первом случае мы свами можем попасть на никогда не выполняемый код. Обычно про такое должен выдавать предупреждение компилятор.
Функция ЭтоТипБулево(Значение) Экспорт Если ТипЗнч(Значение)=Тип("Булево") Тогда Возврат Истина; Иначе Возврат Ложь; КонецЕсли; // сюда мы никогда доходить не будем Сообщить("А сердечник можно делать из дерева! | Все равно этот код никогда не выполнится..."); Возврат Неопределено; КонецФункции
- Удобно использовать оператор "Возврат" в начале функции при выполнении обязательных проверок на корректность входных параметров. В этом случае легко отвязывается задача проверки параметров от всего куска кода процедуры.
Функция ПолучитьСреднийЧек(Партнер) Экспорт // I) Проверяем входные параметры // уходим если ошибка, обрабатывать не стоит Если НЕ ЗначениеЗаполнено(Партнер) Тогда Возврат новый Структура(Неопределено,"Данные пустые"); КонецЕсли; // II) Выполняем основную задачу // ... // ... // ... КонецФункции
3. Оператор Прервать (Break)
Один из самых востребованных операторов, если рассмотреть программирование на C++. В свое время я часто его использовал в связке с Switch и Операторах цикла (For, While). Обратите внимание, что оператор "Прервать" может использоваться и для выхода из бесконечного цикла.
Рассмотрим вариацию типового примера для подключения к клиенту тестирования механизма автоматизированного тестирования от 1С.
Подключен = Ложь;
ОписаниеОшибкиСоединения = "";
ТестовоеПриложение = Новый ТестируемоеПриложение(,НомерПорта,);
ВремяОкончанияОжидания = ТекущаяДата() + 70; // 70 секунд ожидаем подключение
Пока Истина Цикл
Попытка
ТестовоеПриложение.УстановитьСоединение();
Подключен = Истина;
Прервать;
Исключение
ОписаниеОшибкиСоединения = ОписаниеОшибки();
КонецПопытки;
Если ТекущаяДата() >= ВремяОкончанияОжидания Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Подключен = Ложь;
ОписаниеОшибкиСоединения = "";
ТестовоеПриложение = Новый ТестируемоеПриложение(,НомерПорта,);
ВремяОкончанияОжидания = ТекущаяДата() + 70; // 70 секунд ожидаем подключение
Пока (ТекущаяДата() <= ВремяОкончанияОжидания) И (Подключен=Ложь) Цикл
Попытка
ТестовоеПриложение.УстановитьСоединение();
Подключен = Истина;
Исключение
ОписаниеОшибкиСоединения = ОписаниеОшибки();
КонецПопытки;
КонецЦикла;
Какие отличия мы видим?
- в первом случае мы последовательно встречаемся с условиями, так проводить анализ значительно проще на мой взгляд
- во втором случае код внутри процедуры получился значительно меньше и сразу видно условие выхода из цикла
- Использование "вечных" условий в операторах цикла - это дурной тон для языка 1С Предприятие.
4. Оператор Продолжить (Continue)
Оператор "Продолжить" (continue) позволяет сразу перейти в конец тела цикла, пропуская весь код, который находится под ним. Это полезно в тех случаях, когда мы хотим завершить текущую итерацию раньше времени.
Будьте осторожны при использовании оператора "Продолжить" с циклом "Пока" (while). Поскольку в этих циклах инкремент счетчиков выполняется непосредственно в теле цикла, то использование "Продолжить" может привести к тому, что цикл станет бесконечным!
Рассмотрим задачу - Требуется выполнить предварительную обработку таблицы данных загруженную из Excel.
В таблице хранится наименование контрагента и некоторые дополнительные данные (сумма и т.п.). В рамках обработки будем искать ссылку на контрагента в базе данных, если данные битые, то будем пропускать поиск.
ш=0;
Пока ш<ТаблицаДанных.Количество()-1 Цикл
стр = ТаблицаДанных[ш];
// такая ошибка сделает наш цикл бесконечным
Если ЭтоБитыеДанные(стр) Тогда
Продолжить;
КонецЕсли;
// Обрабатываем данные во внешней процедуре
НайтиСсылкуКонтрагентаПоНаименованию(стр);
// Еще много кода пост обработки
// ...
// ...
// ...
// ...
ш=ш+1;
КонецЦикла;
Процедура "НайтиСсылкуПоНаименованию" ищет ссылку контрагента в базе 1С.
Функция "ЭтоБитыеДанные" проверяет наличие "кривой" информации в полях Сумма, КонтрагентНаименование.
Как видно, то в результате необдуманного выбора оператора цикла, установки счетчика и размещения условия прервать, мы уйдем в бесконечный цикл. Лучше переписать так:
ш=0;
Для ш=0 по ш<ТаблицаДанных.Количество()-1 Цикл
стр = ТаблицаДанных[ш];
// такая ошибка сделает наш цикл бесконечным
Если ЭтоБитыеДанные(стр) Тогда
Продолжить;
КонецЕсли;
// Обрабатываем данные во внешней процедуре
НайтиСсылкуКонтрагентаПоНаименованию(стр);
// Еще много кода пост обработки
// ...
// ...
// ...
// ...
КонецЦикла;
Теперь рассмотрим вариант без использования оператора "Продолжить".
ш=0;
Пока ш<ТаблицаДанных.Количество()-1 Цикл
стр = ТаблицаДанных[ш];
// такая ошибка сделает наш цикл бесконечным
Если НЕ ЭтоБитыеДанные(стр) Тогда
// Обрабатываем данные во внешней процедуре
НайтиСсылкуКонтрагентаПоНаименованию(стр);
// Еще много кода пост обработки
// ...
// ...
// ...
// ...
КонецЕсли;
ш=ш+1;
КонецЦикла;
Выводы:
- В первом случае мы легко можем допустить ошибку, которая приведет к бесконечному циклу (эта ошибка будет "плавающая", т.е. не всегда будет срабатывать). Поэтому лучше выбрать другой оператор цикла.
- Большой код, рекомендуем выносить рефакторингом.
- Второй вариант выглядит более логически понятным и "завершенным".
5. Операторы прервать и продолжить
Многие учебники рекомендуют не использовать операторы "Прервать" (break) и "Продолжить" (continue), поскольку они приводят к произвольному перемещению точки выполнения программы по всему коду, что усложняет понимание и следование логике выполнения такого кода.
Тем не менее, разумное использование операторов "Продолжить" и "Прервать" может улучшить читабельность циклов в программе, уменьшив при этом количество вложенных блоков и необходимость наличия сложной логики выполнения циклов.
Например, рассмотрим следующую задачу: Требуется провести некоторую обработку данных после загрузки из таблицы Excel и еще будет считать баланс. В случае ошибки нужно выдать сообщение оператору о неконсистентности этих данных.
Функция ПолучимБаланс(ТаблицаДанных) Экспорт
Баланс = 0;
// 1. Обрабатываем массив данных с типами элементов: Позиция(Число) ,Выбрана (булево), Контрагент (Справочник), Сумма (число).
// 2. Количество записей 1 000 000 элементов
Для каждого стр из ТаблицаДанных Цикл
Если НЕ стр.Выбрана Тогда
Продолжить;
КонецЕсли;
// если битая ссылка, то нужно выдать ошибку
Если ЭтоБитыеДанные(стр)=Истина Тогда
Баланс=Неопределено;
Сообщить("Позиция "+стр.Позиция+" битые данные!");
Прервать;
КонецЕсли;
Баланс = Баланс+стр.Сумма;
КонецЦикла;
Возврат Баланс;
КонецФункции
Функция "ЭтоБитыеДанные" проверяет наличие "кривой" информации в полях Сумма, Контрагент.
Функция ПолучимБаланс(ТаблицаДанных) Экспорт
Баланс = 0;
БитыеДанные = Ложь;
// 1. Обрабатываем массив данных с типами элементов: Позиция(Число) ,Выбрана (булево), Контрагент (Справочник), Сумма (число).
// 2. Количество записей 1 000 000 элементов
Для каждого стр из ТаблицаДанных Цикл
Если стр.Выбрана Тогда
БитыеДанные = ЭтоБитыеДанные(стр);
Если БитыеДанные =Ложь Тогда
Баланс = Баланс+стр.Сумма;
Иначе
БитыеДанные =Истина;
Баланс = Неопределено;
Сообщить("Позиция "+стр.Позиция+" битые данные!");
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат Баланс;
КонецФункции
Код выше требует рефакторинга, т.к. гонять цикл впустую (вдруг битые данные будут сразу со второго элемента) - это "жесть" конечно. Иными словами для следования структурному подходу требуется изменение структуры.
Функция ПолучимБаланс(ТаблицаДанных) Экспорт
Баланс = 0;
ш=0;
// 1. Обрабатываем массив данных с типами элементов: выбрана (булево), Сумма (число).
// 2. Количество записей 1 000 000 элементов
Пока ш<ТаблицаДанных.Количество()-1 И ЭтоБитыеДанные(ТаблицаДанных[ш])=Ложь Цикл
Если ТаблицаДанных[ш].Выбрана Тогда
Баланс = Баланс+ТаблицаДанных[ш].Сумма;
КонецЕсли;
КонецЦикла;
// если ш меньше размера таблицы, значит мы не дошли до конца без ошибок
Если ш<ТаблицаДанных.Количество()-1 Тогда
Баланс = Неопределено;
Сообщить("Позиция "+стр.Позиция+" битые данные!");
КонецЕсли;
Возврат Баланс;
КонецФункции
Рассмотрим результат сравнения:
- Не желание использовать операторы переходов вынудило нас во втором случае полностью переписать код
- Во втором случае потребовалось заводить лишние переменные
- Во втором случае потребовалось вынести часть логики из цикла в конец функции, что увеличивает объем кода выполняющий решения поставленной задачи
- В циклах "Для" и "Для каждого" удобно использовать оператор "Продолжить" для быстрого перехода к следующей итерации по результатам условий, если текущая строка не требует обработки.
// ... Для каждого стр из ТаблицаДанных Цикл Если НЕ стр.Выбрана Тогда // Переходим сразу к следующей итерации Продолжить; КонецЕсли; // Далее обрабатываем данные // ... КонецЦикла; // ...
Заключение
Я считаю, что использование или не использование операторов зависит от ситуации. В некоторых случаях- это позволяет существенно упростить написание и понимание кода. Однако, в некоторых случаях небрежное или "слепое" использование операторов переходов может привести к нежелательным последствиям.
- Операторы "Продолжить" и "Прервать" удобно использовать в циклах "Для" и "Для каждого"
- Если речь идет про оператор цикла "Пока", то тут лучше стараться не использовать "Продолжить", т.к. в некоторых комбинациях возможно создание бесконечного цикла.
- Дурной тон в операторах циклов использовать "вечные" условия по типу " Пока Истина Цикл".
- При использовании оператора "Возврат" надо следить, чтобы не оставалось недостижимого кода.
- Также возможны случаи написания недостижимого кода с операторами "Продолжить" и "Прервать".
- Оператор "Возврат" удобно использовать в начале функций при выполнении проверок на консистентность (корректность) входных параметров.
- Оператор "Перейти" запрещено использовать.
- Если придерживаться структурного подхода, то следует себя ограничивать в создании вложенности (или "матрешек") с условиями "Если Тогда", так как это затрудняет понимание кода и увеличивает критерий цикломатичности.
- В процессе разработки (кодирования) всегда выполняем рефакторинг кода и выносим большие блоки в отдельные функции.
- Используйте в процессе работы анализ качества кода (код-ревью) (По следам код-ревью, Как завести у себя в команде код-ревью. Отвечаем на вопросы)
- Используйте тестирование и автоматизированное тестирование (Автоматизация тестирования, Пример создания сценарного UI теста для платформы 1С)