Ответ 1
Ваше использование goto
в порядке. Это не нарушает 2 хороших способа использования goto.
-
goto
ДОЛЖЕН спуститься (несколько строк) в источнике - Самый внутренний блок
goto labels
ДОЛЖЕН содержать инструкцииgoto
Оператор goto
был подробно рассмотрен в нескольких обсуждениях SO (см. this и что), и я, конечно, не хочу оживлять эти горячие дебаты.
Вместо этого я хотел бы сосредоточиться на одном случае использования goto
и обсудить его значение и возможные альтернативы.
Рассмотрим следующий фрагмент кода, который является общим для (по крайней мере, моего) FSM:
while (state = next_state()) {
switch (state) {
case foo:
/* handle foo, and finally: */
if (error) goto cleanup;
break;
case bar:
/* handle bar, and finally: */
if (error) goto cleanup;
break;
/* ...other cases... */
}
}
return ok;
cleanup:
/* do some cleanup, i.e. free() local heap requests, adjust global state, and then: */
return error;
Переключение элемента очистки в отдельную функцию просто для сохранения goto
кажется неудобным. С другой стороны, мы были подняты, чтобы осудить использование goto
, где это возможно.
Мой вопрос: мой пример кода считается хорошим стилем?
Если нет, существуют ли доступные альтернативы?
Пожалуйста, придерживайтесь конкретного использования goto
, описанного выше. Я не хочу вникать в еще одно обсуждение об общем использовании goto
.
Ваше использование goto
в порядке. Это не нарушает 2 хороших способа использования goto.
goto
ДОЛЖЕН спуститься (несколько строк) в источникеgoto labels
ДОЛЖЕН содержать инструкции goto
Вместо того, чтобы извлекать логику очистки в свою собственную функцию и вызывать ее из разных мест, я хотел бы рассмотреть вопрос об извлечении оператора switch в отдельную функцию и возвращении от него кода ошибки. В цикле while вы можете проверить код возврата и выполнить очистку и вернуть, если это необходимо.
Если у вас есть несколько ресурсов, разделяемых между логикой переключения и очистки, я думаю, что goto будет предпочтительнее передавать все это состояние.
Goto не требуется, если у вас есть switch
. Использование switch
и goto
просто добавляет сложности.
while (state) {
switch (state) {
case cleanup:
/* do some cleanup, i.e. free() local heap requests, adjust global state, and then: */
return error;
case foo:
/* handle foo, and finally: */
if (error) { state = cleanup; continue; }
break;
case bar:
/* handle bar, and finally: */
if (error) { state = cleanup; continue; }
break;
/* ...other cases... */
}
state = next_state();
}
return ok;
Я бы сказал, если код очистки не может быть обобщен, т.е. он специфичен для функции, в которой он используется, goto - это хороший и чистый способ сделать это.
Я видел, что goto используется таким образом в ядре OpenBSD, особенно в драйверах устройств ATA (один такой пример), и я лично считаю, что это хороший стиль, поскольку он помогает проиллюстрировать, что именно происходит и как код соответствует соответствующему FSM. При попытке проверить функциональность FSM на спецификацию это использование goto несколько улучшает четкость.
Если весь код инициализации выполняется до ввода цикла while, то ваши gotos бесполезны, вы можете выполнить очистку при выходе из цикла. Если ваш конечный автомат все о том, чтобы воспитывать вещи в правильном порядке, тогда почему бы и нет, но поскольку у вас есть конечный автомат, почему бы не использовать его для очистки?
Я не против goto при инициализации нескольких вещей вместе и с простым кодом обработки ошибок, как обсуждалось здесь. Но если вы столкнулись с проблемой создания конечного автомата, то я не вижу достаточной причины для их использования. ИМО, вопрос все еще слишком общий, более практичным примером конечной машины было бы полезно.
Глядя на ответ Бен Фойгта, я дал альтернативный ответ:
while (state = next_state()) {
switch (state) {
case foo:
/* handle foo, and finally: */
/* error is set but not bothered with here */
break;
case bar:
/* handle bar, and finally: */
/* error is set but not bothered with here */
break;
/* ...other cases... */
}
if (error) {
/* do some cleanup, i.e. free() local heap requests, */
/* adjust global state, and then: */
return error;
}
}
return ok;
Недостатком этого является то, что вы должны помнить, что после его обработки состояние может быть очищено, если есть ошибка. Похоже, что структура if
может быть цепочкой if-else
для обработки различных типов ошибок.
Я не принимал формальный класс в FSM, но мне кажется, что код, который вы опубликовали, имеет такое же поведение.
Если вам нужен только код очистки, который может быть вызван из нескольких мест в вашей процедуре, и ему нужно получить доступ к локальным ресурсам, возможно, вместо этого использовать оператор lambda. Определите его перед логикой коммутатора и просто вызовите его там, где вам нужно очистить. Мне нравится идея по нескольким причинам: 1) Это круче, чем goto (и это всегда важно) 2) Вы получаете чистую инкапсуляцию логики без необходимости создавать внешний метод и передавать множество параметров, поскольку лямбда может обращаться к тем же локальным переменным, что и закрытие.
Использование goto
для очистки кода путем разбиения множественного вложения на цикл очень удобно, вместо того, чтобы устанавливать флаги и проверять их в каждой вложенности. Например, если вы не можете открыть файл, и вы обнаружите его глубоко в вложенности, вы можете просто goto
очистить сегмент и закрыть файлы и свободные ресурсы. Вы можете увидеть примеры goto
в источниках инструментов coreutilities.
Общий принцип, которым я хотел бы следовать, заключается в том, что нужно, когда это возможно, попытаться написать код, поток и дизайн которого соответствует требованиям проблемной области ( "что должна делать программа" ). Языки программирования включают в себя структуры управления и другие функции, которые хорошо подходят для большинства, но не для всех проблемных областей. Такие структуры следует использовать, когда они соответствуют требованиям программы. В тех случаях, когда требования к программе не подходят для языковых функций, я предпочитаю сосредоточиться на написании кода, который выражает то, что нужно сделать программе, чем на то, чтобы подгонять код к шаблонам, которые, хотя они отвечают требованиям других приложений, действительно не соответствуют требованиям к написанию программы.
В некоторых случаях очень естественный способ перевода состояний машин в код, в случаях, когда подпрограмма будет иметь возможность не "выходить", пока конечный автомат не выполнит какую-либо форму заключения, должен иметь Метка goto
представляет каждое состояние и использует операторы if
и goto
для состояний переходов. Если требуемые переходы состояний лучше подходят для других структур управления (например, циклы while
), использование таких циклов будет лучше, чем операторы goto
, а с помощью операторов switch
могут выполняться определенные виды "адаптации" (например, с рутинной выполнением перехода состояния каждый раз, когда он выполнялся, а не требовать от него немедленного запуска до завершения) намного проще. С другой стороны, поскольку оператор switch
на самом деле просто переодетый "goto", может быть проще просто использовать goto
напрямую, чем использовать оператор switch для имитации.
Я далеко за то, что стал слепым фанатиком no-goto, но я считаю, что в этом случае нужно использовать механизм, который явно предназначен для обработки ошибок.
Предполагаемый код С#, но его можно легко модифицировать для всех других языков соответствия (Java, C++):
try
{
while (state = next_state())
{
switch (state)
{
case foo:
/* handle foo, and finally: */
if (error)
throw new Exception();
break;
case bar:
/* handle bar, and finally: */
if (error)
throw new Exception();
break;
/* ...other cases... */
}
}
return ok;
}
catch (Exception e)
{
/* do some cleanup, i.e. free() local heap requests, adjust global state, and then:
*/
return error;
}
Честно говоря, я никогда не использовал goto, потому что я на самом деле никогда не нуждался в этом. В большинстве случаев всегда существует очевидное решение, которое решает проблему.
Имейте в виду, что существуют некоторые языковые конструкции, которые выполняют, скажем, контролируемый goto, например throw/catch/finally (очевидно), переключатель (который просто безоговорочно переходит к конкретному случаю) и циклы (очевидно). Эффективно, если, как представляется, существует необходимость в goto, в большинстве случаев фактическая потребность в конкретном использовании goto, которую уже предоставляет одна из существующих конструкций.
Тем не менее, я все еще поддерживаю мнение, что руководящие принципы кодирования не являются законами, и они не должны служить оправданием для отключения вашего мозга. Тот факт, что я не нашел действительного случая для использования goto, определенно не означает, что их нет. Если использование goto выражает намерение программиста более четко, чем любая другая конструкция, используйте его.