Ч. 1. Гл. 7. Форматирование кода и оператор Goto

Часть 1. Глава 7. Форматирование кода и Goto
[Данную главу надо читать в текстовом редакторе с моноширинным шрифтом, например, "Courier New"; иначе отступы могут пропасть или изменить свою величину]

Как известно из учебников, оператор безусловного перехода goto использовать плохо, так как это нарушает принципы структурного программирования.

Всё логично, хотя многие не могут сказать, а почему плохо нарушать принципы структурного программирования?

Да не почему. Некоторые личности считают, что программа, построенная по строгим принципам лучше читается и содержит меньше ошибок. Но это их личное мнение. Некоторые считают, что в современном языке не должно быть оператора goto, ведь это противоречит принципам структурного программирования.

А некоторые считают, что программа сама по себе должна лучше читаться, даже если это идёт в разрез с принципами структурного программирования.
Приведу пример.

Представим себе, что нам нужно выбрать оружие, затем выбрать патрон и, наконец, выбрать режим стрельбы. После чего поразить цель. Хотя это не совсем правильно, но мы считаем, что программа сначала применяет менее простое оружие, потом более простое.

Объект State будет хранить данные о том, какие операции уже произведены.

Тогда, попробуем написать программу на псевдокоде, которая реализует эту концепцию.

Итак.
var state = new State();

ВыбратьОружие(state);
ВыбратьПатрон(state);
ВыбратьРежимОгня(state);
ПоразитьЦель(state);

Это если с первого раза всё получится. А если нет?

var state = new State();

1:
        int r = 0;
        r = ВыбратьОружие(state);
        if (r < -1)
                goto 1;
        if (r == -1)
                goto Error;
2:
        r = ВыбратьПатрон(state);
        if (r < -1)
                goto 2;
        if (r == -1)
                goto 1;
3:
        r = ВыбратьРежимОгня(state);
        if (r < -1)
                goto 3;
        if (r == -1)
                goto 2;
4:
        r = ПоразитьЦель(state);
        if (r < -1)
                goto 4;
        if (r == -1)
                goto 3;

        return;

Error:
        Здесь обработка ошибок.

Как видим, мы здесь обрабатываем коды ошибок. -1 - неисправимый отказ, например, перебрали все варианты. Отрицательное число - просто отказ.
Программа сначала выберет минимальное оружие и напишет об этом в объекте state. Если оружие неработоспособно или отсутствует, вернётся -1. После этого программа перейдёт на метку "1" и снова попробует выбрать оружие, но уже с учётом предыдущего выбора. И так далее.

Но как это записать без goto?
Попробуем    вложенные циклы. Сначала запишем самые короткие циклы.

var state = new State();

int r = 0;
1:
do
{
        r = ВыбратьОружие(state);
        if (r == -1)
                return -1;
}
while (r < 0);

2:
do
{
        r = ВыбратьПатрон(state);
}
while (r < -1);

if (r == -1)
        goto 1;
3:
do
{
        r = ВыбратьРежимОгня(state);
}
while (r < -1);

if (r == -1)
        goto 2;

do
{
        r = ПоразитьЦель(state);
}
while (r < -1);

if (r == -1)
        goto 3;

return 0;

Обработка ошибок здесь предполагается где-то в другой функции.
Заметьте, что переписанный код занимает уже 41-ну строку кода вместо 32-х, а делает тоже самое. И это мы ещё не избавились от goto.
Попробуем записать один более крупный цикл.


var state = new State();

int r = 0;
do
{
        do
        {
                r = ВыбратьОружие(state);
                if (r == -1)
                return -1;
        }
        while (r < 0);

        2:
        do
        {
                r = ВыбратьПатрон(state);
        }
        while (r < -1);
while (r == -1);

3:
do
{
        r = ВыбратьРежимОгня(state);
}
while (r < -1)

if (r == -1)
        goto 2;

do
{
        r = ПоразитьЦель(state);
}
while (r < -1)

if (r == -1)
        goto 3;

return 0;

Теперь попробуем записать второй.

var state = new State();

int r = 0;
do
{
        do
        {
                r = ВыбратьОружие(state);
                if (r == -1)
                return -1;
        }
        while (r < 0);
       
// Здесь нам надо вставить новое do

        2:
        do
        {
                r = ВыбратьПатрон(state);
        }
        while (r < -1);
while (r == -1);

3:
do
{
        r = ВыбратьРежимОгня(state);
}
while (r < -1)

if (r == -1)
        goto 2;

// А здесь нам надо вставить новый while (r == -1);

...

Но что это?
Циклы не вкладываются друг в друга, а пересекаются.
Структурное программирование не проходит. Хотя goto спокойно справился и программа вполне читаема и понятна. Вот она, где польза от оператора goto, где можно и нужно применять goto не стесняясь.

Если хотите, попробуйте написать это с использованием принципа структурного программирования. Однако, как это сделать уже не очевидно. То есть надо думать.
А зачем думать, если с goto думать не обязательно и всё нормально читается?
Лишние размышления - лишние ошибки и лишнее потраченное время, которое ничем не обосновано.
Даже обычные вложенные циклы читаются, с моей точки зрения, уже несколько хуже, чем такое goto. Конечно, я не агитирую за то, чтобы каждый вложенный цикл превращать в goto. Я использовал goto за последние лет пять раза три. В большинстве случаев, если вы хотите использовать goto, то надо задуматься, возможно, вы делаете что-то не так.
Однако, слепо верить каким-то надуманным принципам "структурного программирования" тоже не стоит.

Кроме этого, goto также позволяет напомнить программисту, куда именно осуществляется переход.

var state = new State();

int r = 0;
do
{
        do
        {
                r = ВыбратьОружие(state);
                if (r == -1)
                return -1;
        }
        while (r < 0);

        2:
        do
        {
                r = ВыбратьПатрон(state);
        }
        while (r < -1);
while (r == -1);

// ---------------------------------------

var state = new State();

ВыбратьОружие_:

        int r = 0;
        r = ВыбратьОружие(state);
        if (r < -1)
                goto ВыбратьОружие_;
        if (r == -1)
                goto Error;

ВыбратьПатрон_:
        r = ВыбратьПатрон(state);
        if (r < -1)
                goto ВыбратьПатрон_;
        if (r == -1)
                goto ВыбратьОружие_;

Допустим, программист хочет проверить, как будет работать код, если кончились патроны.
Второй код программист пожет читать последовательно, примерно так.
Сначала выбираем оружие. Затем выбираем патрон. Если произошла фатальная ошибка r == -1 (патронов не осталось), переходим к этапу ВыбратьОружие.

Первый вариант кода читается примерно так.
Сначала выбираем оружие. Затем выбираем патрон. Затем, что это? Пока r == -1, то есть пока фатальная ошибка, мы повторяем цикл. Какой цикл?
Программист простматривает код вверх и видит, да - это цикл, начинающийся с выбора оружия.

Как видите, программисту надо немного больше соображать. А значит, тратить чуть больше времени и больше уставать. При этом дополнительные операции, пусть даже простые, отвлекают внимание программиста от главного.


Аналогично, правило операторная скобка на отдельной строке. Это очень хорошее правильно, которое очень часто игнорируется.
Есть исключения, где это правило можно не использовать. Например, в C#

public bool isFailed {    get => checkFail();    }

В принципе, здесь можно писать на одной строке. Хотя очень часто даже такие мелкие вызовы лучше выносить отдельно, потому что какой-то неверный символ теряется в каше скобок и незаметен.

В большинстве же случаев правило должно соблюдаться:
while ...
{
        Здесь какие-то операции
        Здесь и ещё какие-то операции
}

Почему?
1. Это отделяет заголовок цикла от тела цикла. Поэтому часто его проще читать, так как он привлекает внимание и стоит отдельно.
2. В некоторых ситуациях, программисты, хотя и редко, в запарке, бывают забывают ставить скобки вообще. Получается что-то такое:

while ...
        Здесь какие-то операции
        Здесь и ещё какие-то операции
       
Шансы это заметить больше, если вы привыкли видеть скобку и пустое место сразу после while.
Вообще говоря, если программист читает код типа

while ... {
        Здесь какие-то операции
        Здесь и ещё какие-то операции
}

Если программист просматривает этот код, он либо должен найти конец заголовка и проверить, что там есть скобка, либо должен поверить в то, что там есть скобка и привыкнуть в это верить каждый раз.

То есть, в случае, если мы ставим операторную скобку на отдельной строке, у нас единственный недостаток - это лишняя строка. Да и та может быть достоинством, так как делает заголовок цикла более заметным.

В случае же, если мы ставим операторную скобку на той же строке, что и цикл, у нас куча недостатков. Либо это лишняя работа по тому, чтобы убедиться в том, что есть скобка, либо это лишняя работа по тому, чтобы привыкнуть верить, что там есть скобка. И иногда нужно уметь эту привычку отменять, если оказалось, что программист неверно поставил скобки и нам нужно найти такую ошибку. А такие неосознанные привычки отменять научиться очень сложно.


Для фанатов абстрактных принципов можно сказать и то, что именно такой способ расстановки скобок является единственно логичным.

Задумаемся, как мы пишем while, если выполняется только одна функция?

while (...)
        Функция();
       
Либо

while (...) Функция();

Операторные скобки { ... } - это групповой оператор. То есть та самая функция. Соответственно, она либо записывается на одной строке

while {...} {Что-то делаем;}

Но тогда получается немного каша, так как там будет слишком много кода в одной строке.
Либо используем варинат

while (...)
        Функция();
       
То есть

while (...)
{
        Что-то делаем
}

Ведь нам никогда не придёт в голову написать
while (...)        Ф\
        ункция();

даже если компилятор позволяет такие шедевры. Весь код расположен либо на одной строке с while, либо весь ниже его.


Мы никогда не пишем
while (...)
        {
        Что-то делаем
        }

Так как вложенные в групповой оператор команды - это, логически, отдельная часть кода, каждая команда которой вложена в групповой оператор. То есть такие команды имеет более высокий уровень вложенности, а значит, пишутся с большим отступом, чем сами операторные скобки.
В общем, именно так

while (...)
{
        Что-то делаем
}

и никак иначе.
В языках с синтаксисом на основе отступов, часто бывает ситуация, когда вместо операторной скобки ставят пустую строку:

while ... :

        Что-то делаем
        Ещё что-то делаем


Программист может прибегать к такому, чтобы выделить тело цикла относительно другой части программы. Чтобы не было мешанины команд, где трудно с первого взгляда различить, где начало и конец цикла. Грубо говоря, вместо

ЗдесьМыДелаемЛогирование();
Операция1();
while КакоеТоЖуткоеУсловиеАМожетБытьЕщёЧто-то:
        ЧтоТоДелаем();
        ЕщёЧтоТоДелаем();
ИДелаемЕщёЧтоТоАПотомЕщёЧтоТо();
ВсёЛогируем();

используется другой подход

ЗдесьМыДелаемЛогирование();
Операция1();

while КакоеТоЖуткоеУсловиеАМожетБытьЕщёЧто-то:

        ЧтоТоДелаем();
        ЕщёЧтоТоДелаем();


ИДелаемЕщёЧтоТоАПотомЕщёЧтоТо();
ВсёЛогируем();



Коснусь ещё одного момента с форматированием.

Допустим у нас вызов функции повторяется несколько раз.

ВызовФункции1(ЭтоБольшоеИГлавныйОбъект, 1, ЗдесьВычислитьНечто);
ВызовФункции1(ЭтоПервыйОбеъкт, 2, ничего);
ВызовФункции1(ничего, 3, ЗдесьВычислитьНечто);
ВызовФункции1(СовсемНичего, 4, ничего);

Этот код сформатирован неверно. Верно, когда все параметры идут по столбцам.
ВызовФункции1(ЭтоБольшоеИГлавныйОбъект, 1, ЗдесьВычислитьНечто);
ВызовФункции1(ЭтоПервыйОбеъкт,                2, ничего);
ВызовФункции1(ничего,                3, ЗдесьВычислитьНечто);
ВызовФункции1(СовсемНичего,                4, ничего);

Теперь мы легко можем видеть, что второй параметр последовательно меняется с 1 до 4. Иногда, сформатированный таким образом код позволяет быстро выявить ошибки именно путём чтения по-вертикали, а не по-горизонтали.



Кроме этого, почему-то, некоторые программисты считают, что return в коде влечёт за собой ошибки.
Например, некоторые считают, что плохо писать функции вида:

if (!Проверка1())
        return false;

if (!Проверка2())
        return false;


В реальности, это, как раз, зачастую, самое простое написание функции. В том числе, потому что дальше можно не смотреть, какие там if идут и что куда вложено. В результате, функцию проще читать и меньше вероятность что-то неверно в ней поменять.

Главное, запомнить, что оператор "return" должен быть на отдельной строке. После него должна идти пустая строка, чтобы подчеркнуть этот оператор.

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


Рецензии