Откуда пришло понятие «только одно возвращение»?

1055

Я часто общаюсь с программистами, которые говорят: « Не помещайте несколько операторов return в один и тот же метод». Когда я прошу их рассказать мне причины, все, что я получаю, это « Стандарт кодирования говорит об этом » или « Это сбивает с толку ». Когда они показывают мне решения с помощью одного оператора return, код кажется мне более уродливым. Например:

if (condition)
   return 42;
else
   return 97;

« Это ужасно, вы должны использовать локальную переменную! »

int result;
if (condition)
   result = 42;
else
   result = 97;
return result;

Как это 50% -ое раздувание кода облегчает понимание программы? Лично мне сложнее, потому что пространство состояний только что увеличилось на другую переменную, которую можно было бы легко предотвратить.

Конечно, обычно я просто написал бы:

return (condition) ? 42 : 97;

Но многие программисты избегают условного оператора и предпочитают длинную форму.

Откуда пришло это понятие «только одно возвращение»? Есть ли историческая причина, по которой возникла эта конвенция?

fredoverflow
источник
2
Это в некоторой степени связано с рефакторингом Guard Guardlaus. stackoverflow.com/a/8493256/679340 Guard Clause добавит возврат в начало ваших методов. И это делает код намного чище, на мой взгляд.
Петр Перак
3
Это произошло от понятия структурного программирования. Некоторые могут утверждать, что наличие только одного возврата позволяет вам легко модифицировать код, чтобы сделать что-то непосредственно перед возвратом или легко отладить.
Мартынкунев
3
я думаю, что пример - достаточно простой случай, когда у меня не было бы сильного мнения так или иначе. идеал «один вход - один выход» - это больше, чтобы увести нас от безумных ситуаций, таких как 15 операторов возврата и еще две ветви, которые вообще не возвращаются!
Мендота
2
Это одна из худших статей, которые я когда-либо читал. Кажется, что автор тратит больше времени, фантазируя о чистоте своего ООП, чем на самом деле выясняя, как чего-либо достичь. Деревья выражений и вычислений имеют значение, но не тогда, когда вы можете просто написать обычную функцию.
DeadMG
3
Вы должны полностью удалить условие. Ответ 42.
Камбунктив

Ответы:

1120

«Один вход, один выход» был написан, когда большинство программ было написано на ассемблере, FORTRAN или COBOL. Это было неправильно истолковано, потому что современные языки не поддерживают практики, против которых Дейкстра предупреждал.

«Один вход» означал «не создавать альтернативные точки входа для функций». На языке ассемблера, конечно, можно ввести функцию по любой инструкции. FORTRAN поддерживает несколько записей для функций с ENTRYоператором:

      SUBROUTINE S(X, Y)
      R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
      ENTRY S2(R)
      ...
      RETURN
      END

C USAGE
      CALL S(3,4)
C ALTERNATE USAGE
      CALL S2(5)

«Единый выход» означал, что функция должна возвращаться только в одно место: оператор, следующий сразу за вызовом. Это не значит, что функция должна возвращаться только из одного места. Когда было написано структурированное программирование , для функции было обычной практикой указывать ошибку, возвращаясь в другое место. FORTRAN поддерживал это с помощью «альтернативного возврата»:

C SUBROUTINE WITH ALTERNATE RETURN.  THE '*' IS A PLACE HOLDER FOR THE ERROR RETURN
      SUBROUTINE QSOLVE(A, B, C, X1, X2, *)
      DISCR = B*B - 4*A*C
C NO SOLUTIONS, RETURN TO ERROR HANDLING LOCATION
      IF DISCR .LT. 0 RETURN 1
      SD = SQRT(DISCR)
      DENOM = 2*A
      X1 = (-B + SD) / DENOM
      X2 = (-B - SD) / DENOM
      RETURN
      END

C USE OF ALTERNATE RETURN
      CALL QSOLVE(1, 0, 1, X1, X2, *99)
C SOLUTION FOUND
      ...
C QSOLVE RETURNS HERE IF NO SOLUTIONS
99    PRINT 'NO SOLUTIONS'

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

кевин клин
источник
39
И не забудьте код спагетти . Не было неизвестно, чтобы подпрограммы выходили, используя GOTO вместо возврата, оставляя параметры вызова функции и адрес возврата в стеке. Одиночный выход был объявлен как способ, по крайней мере, направить все пути кода в инструкцию RETURN.
TMN
2
@ TMN: в первые дни у большинства машин не было аппаратного стека. Обычно рекурсия не поддерживается. Аргументы подпрограммы и адрес возврата хранились в фиксированных местах рядом с кодом подпрограммы. Возвращение было просто косвенным переходом.
Кевин Клайн
5
@kevin: Да, но, по вашему мнению, это даже не значит, что было изобретено. (Кстати, я на самом деле достаточно уверен, что Фред спросил, что предпочтение для текущей интерпретации «Единого выхода» исходит от.) Кроме того, C имел constс тех пор, как многие из пользователей здесь родились, так что больше не нужны капитальные константы даже в Си. Но Java сохранила все эти старые дурные привычки .
ВОО
3
Так что исключения нарушают эту интерпретацию единого выхода? (Или их более примитивный двоюродный брат setjmp/longjmp,?)
Мейсон Уилер
2
Несмотря на то, что операционная компания спросила о текущей интерпретации единственного возврата, этот ответ имеет наиболее исторические корни. Нет смысла использовать одиночный возврат как правило , если только вы не хотите, чтобы ваш язык соответствовал удивительному VB (не .NET). Только не забывайте использовать логическую логику без короткого замыкания.
acelent
912

Это понятие Single Entry, Single Exit (SESE) происходит от языков с явным управлением ресурсами , таких как C и ассемблер. В C такой код будет пропускать ресурсы:

void f()
{
  resource res = acquire_resource();  // think malloc()
  if( f1(res) )
    return; // leaks res
  f2(res);
  release_resource(res);  // think free()
}

На таких языках у вас есть три основных варианта:

  • Скопируйте код очистки.
    Тьфу. Избыточность это всегда плохо.

  • Используйте, gotoчтобы перейти к коду очистки.
    Это требует, чтобы код очистки был последним в функции. (И вот почему некоторые утверждают, что gotoимеет свое место. И оно действительно - в C.)

  • Введите локальную переменную и управляйте потоком управления через нее.
    Недостатком является то, что управление потоком манипулируют с помощью синтаксиса (думаю break, return, if, while) намного легче следить , чем поток управления манипулируют через состояние переменных (поскольку эти переменные не имеют состояния , когда вы смотрите на алгоритме).

В сборке это даже страннее, потому что вы можете перейти к любому адресу в функции при вызове этой функции, что фактически означает, что у вас есть практически неограниченное количество точек входа в любую функцию. (Иногда это полезно. Такие приемы являются распространенным приемом для компиляторов для реализации thisнастройки указателя, необходимой для вызова virtualфункций в сценариях множественного наследования в C ++.)

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


Однако, когда в языке есть исключения, (почти) любая функция может быть преждевременно закрыта в (почти) любой точке, поэтому вам все равно необходимо предусмотреть возможность преждевременного возврата. (Я думаю, что finallyв основном используется для этого в Java и using(при реализации IDisposable, в finallyпротивном случае) в C #; в C ++ вместо этого используется RAII .) После того, как вы это сделаете, вы не сможете не выполнить очистку после себя из-за раннего returnутверждения, так что, вероятно, самый сильный аргумент в пользу SESE исчез.

Это оставляет читабельность. Конечно, функция 200 LoC с полудюжиной returnоператоров, случайно разбросанных по ней, не является хорошим стилем программирования и не подходит для читабельного кода. Но такую ​​функцию было бы нелегко понять без этих преждевременных возвратов.

В тех языках, где ресурсы не управляются или не должны управляться вручную, нет смысла или нет смысла придерживаться старого соглашения SESE. OTOH, как я уже говорил выше, SESE часто делает код более сложным . Это динозавр, который (за исключением C) плохо вписывается в большинство современных языков. Вместо того, чтобы помочь пониманию кода, это мешает.


Почему Java-программисты придерживаются этого? Я не знаю, но из моего (вне) POV, Java взяла много соглашений из C (где они имеют смысл) и применила их к своему OO-миру (где они бесполезны или просто плохи), где теперь придерживается их, независимо от того, что стоит. (Как соглашение, чтобы определить все ваши переменные в начале области.)

Программисты придерживаются всевозможных странных обозначений по иррациональным причинам. (Глубоко вложенные структурные утверждения - «наконечники стрел» - в таких языках, как Паскаль, когда-то рассматривались как прекрасный код.) Применение чистых логических рассуждений к этому, кажется, не может убедить большинство из них отклониться от своих устоявшихся способов. Вероятно, лучший способ изменить такие привычки - это научить их делать то, что лучше, а не то, что принято. Вы, будучи учителем программирования, держите это в руках.:)

sbi
источник
52
Правильно. В Java код очистки относится к finallyразделам, где он выполняется независимо от ранних returns или исключений.
Ден04
15
@ dan04 в Java 7 вам даже не нужно finallyбольшую часть времени.
Р. Мартиньо Фернандес
93
@ Steven: Конечно, вы можете продемонстрировать это! Фактически, вы можете показать сложный и сложный код с любой функцией, которая также может быть показана, чтобы сделать код проще и понятнее. Всем можно злоупотреблять. Суть в том, чтобы написать код, чтобы его было легче понять , и когда это связано с выбрасыванием SESE в окно, пусть будет так, и черт побери старые привычки, которые применялись к разным языкам. Но я бы без колебаний контролировал выполнение по переменным, если бы думал, что это облегчит чтение кода. Просто я не помню, чтобы видел такой код почти два десятилетия.
ВОО
21
@Karl: Действительно, это серьезный недостаток языков GC, таких как Java, которые избавляют вас от необходимости очищать один ресурс, но терпят неудачу со всеми остальными. (С ++ решает эту проблему для всех ресурсов с помощью RAII .) Но я даже не говорю только памяти (я только положить malloc()и free()в комментарий в качестве примера), я говорил о ресурсах в целом. Я также не предполагал, что GC решит эти проблемы. (Я упомянул C ++, в котором нет GC из коробки.) Из того, что я понимаю, в Java finallyиспользуется для решения этой проблемы.
ВОО
10
@sbi: Для функции (процедуры, метода и т. д.) важнее, чем длина не более одной страницы, для функции должен иметь четко определенный контракт; если это не делает что-то ясное, потому что это было нарезано, чтобы удовлетворить ограничение произвольной длины, это плохо. Программирование - это игра разных, иногда противоречивых сил друг против друга.
Донал Феллоуз
81

С одной стороны, одиночные операторы возврата упрощают ведение журнала, а также формы отладки, основанные на ведении журнала. Я помню, как много раз мне приходилось сводить функцию к единому возврату, просто чтобы распечатать возвращаемое значение в одной точке.

  int function() {
     if (bidi) { print("return 1"); return 1; }
     for (int i = 0; i < n; i++) {
       if (vidi) { print("return 2"); return 2;}
     }
     print("return 3");
     return 3;
  }

С другой стороны, вы могли бы реорганизовать это в function()эти вызовы _function()и записать результат.

perreal
источник
31
Я также добавил бы, что это облегчает отладку, потому что вам нужно всего лишь установить одну точку останова, чтобы перехватить все выходы * из функции. Я полагаю, что некоторые IDE позволяют вам поставить точку останова на закрывающую скобку функции, чтобы сделать то же самое. (* если вы не
вызовете
3
По той же причине это также облегчает расширение (добавление) функции, поскольку ваши новые функции не нужно вставлять перед каждым возвратом. Скажем, вам нужно обновить журнал с результатом вызова функции, например.
ДжеффСахол
63
Честно говоря, если бы я поддерживал этот код, я бы предпочел иметь разумно определенные _function(), с returns в соответствующих местах и ​​оберткой с именем, function()которая обрабатывает постороннее ведение журнала, чем иметь один function()с искаженной логикой, чтобы все возвраты помещались в один выход -точка, чтобы я мог вставить дополнительное утверждение до этой точки.
Руах
11
В некоторых отладчиках (MSVS) вы можете поставить
точку
6
печать! = отладка. Это не аргумент вообще.
Петр Перак
53

«Один вход, один выход» возник в результате революции в области структурированного программирования в начале 1970-х годов, которая была начата письмом Эдсгера В. Дейкстры в редакцию « Заявление GOTO считается опасным ». Концепции структурного программирования были подробно изложены в классической книге «Структурное программирование» Оле Йохана-Даля, Эдсгера В. Дейкстры и Чарльза Энтони Ричарда Хоара.

"GOTO Заявление считается вредным" необходимо читать, даже сегодня. «Структурированное программирование» устарело, но все еще очень, очень полезно, и должно быть на вершине списка «Обязательно читать» любого разработчика, намного выше всего, например, от Стива Макконнелла. (В разделе Даля изложены основы классов в Simula 67, которые являются технической основой для классов в C ++ и всего объектно-ориентированного программирования.)

John R. Strohm
источник
6
Статья была написана за несколько дней до C, когда GOTO активно использовались. Они не враги, но этот ответ определенно правильный. Оператор return, который не находится в конце функции, фактически является goto.
user606723
31
Статья также была написана в те дни, когда gotoможно было буквально идти куда угодно , например, прямо в какую-то случайную точку в другой функции, обходя любое понятие процедур, функций, стека вызовов и т. Д. Ни один здравомыслящий язык не позволяет делать это в прямом смысле goto. С setjmp/ longjmpэто единственный полуисключительный случай, о котором я знаю, и даже это требует сотрудничества с обеих сторон. (Полуиронично, что я использовал слово «исключительный», хотя, учитывая, что исключения делают почти одно и то же ...) По сути, статья не одобряет практику, которая давно умерла.
cHao
5
Из последнего параграфа «Заявление Гото считается опасным»: «в [2] Гизеппе Якопини, кажется, доказал (логическую) избыточность оператора go to. Упражнение для более или менее механического перевода произвольной блок-схемы в скачок. Однако не рекомендуется использовать меньше одного . Тогда нельзя ожидать, что результирующая блок-схема будет более прозрачной, чем исходная. "
hugomg
10
Какое это имеет отношение к вопросу? Да, работа Дейкстры в конечном итоге привела к языкам SESE, и что с того? Так же, как и работа Бэббиджа. И , возможно , вы должны перечитать бумагу , если вы думаете , что говорит что - то о наличии нескольких точек выхода в функции. Потому что это не так.
jalf
10
@ Джон, ты, кажется, пытаешься ответить на вопрос, фактически не отвечая на него. Это хороший список для чтения, но вы не цитировали и не перефразировали что-либо, чтобы оправдать свое утверждение о том, что в этом эссе и книге есть что сказать об озабоченности автора. Действительно, за пределами комментариев вы не сказали ничего существенного по этому вопросу. Подумайте над расширением этого ответа.
Shog9
35

Всегда легко связать Фаулера.

Одним из основных примеров, которые идут против SESE, являются пункты охраны:

Заменить вложенные условные выражения на охранные

Используйте пункты охраны для всех особых случаев

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
return result;
};  

                                                                                                         http://www.refactoring.com/catalog/arrow.gif

double getPayAmount() {
    if (_isDead) return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired) return retiredAmount();
    return normalPayAmount();
};  

Для получения дополнительной информации см. Стр. 250 Рефакторинга ...

Pieter B
источник
11
Еще один плохой пример: это можно легко исправить с помощью else-ifs.
Джек,
1
Ваш пример несправедлив, как насчет этого: double getPayAmount () {double ret = normalPayAmount (); if (_isDead) ret = deadAmount (); if (_isSeparated) ret = separaAmount (); if (_isRetired) ret = retiredAmount (); вернуться в отставку; };
Charbel
6
@ Шарбель Это не одно и то же. Если _isSeparatedи то и _isRetiredдругое может быть правдой (и почему это невозможно?), Вы возвращаете неправильную сумму.
17
2
@Konchog « вложенные условные выражения обеспечат лучшее время выполнения, чем пункты охраны ». Это в основном нуждается в цитировании. У меня есть сомнения, что это достоверно. В этом случае, например, чем ранние возвраты отличаются от логического короткого замыкания с точки зрения сгенерированного кода? Даже если бы это имело значение, я не могу представить себе случай, когда разница будет больше, чем бесконечно малая полоска. Таким образом, вы применяете преждевременную оптимизацию, делая код менее читабельным, просто чтобы удовлетворить некоторые недоказанные теоретические положения о том, что, по вашему мнению, приводит к несколько более быстрому коду. Мы здесь этого не делаем
underscore_d
1
@underscore_d, ты прав. это во многом зависит от компилятора, но может занять больше места. Посмотрите на две псевдосборки, и легко понять, почему выражения guard происходят из языков высокого уровня. Тест «А» (1); branch_fail end; тест (2); branch_fail end; Тест (3); branch_fail end; {CODE} end: return; Тест "B" (1); branch_good next1; вернуть; следующий1: тест (2); branch_good next2; вернуть; следующий2: тест (3); branch_good next3; вернуть; next3: {CODE} return;
11

Я написал пост в блоге на эту тему некоторое время назад.

Суть в том, что это правило относится к эпохе языков, в которых нет сборки мусора или обработки исключений. Нет формального исследования, которое показывает, что это правило приводит к улучшению кода на современных языках. Не стесняйтесь игнорировать это всякий раз, когда это приведет к более короткому или более читаемому коду. Парни из Java, которые настаивают на этом, слепо и беспрекословно следуют устаревшим, бессмысленным правилам.

Этот вопрос также задавался на Stackoverflow

Энтони
источник
Эй, я больше не могу найти эту ссылку. Есть ли у вас версия, которая размещена где-то еще доступны?
Ник Хартли
Привет, QPT, хорошее место. Я вернул сообщение в блоге и обновил URL выше. Надо сейчас ссылку!
Энтони
Хотя это еще не все. С помощью SESE намного проще управлять точным временем выполнения. В любом случае вложенные условные выражения часто могут быть подвергнуты рефакторингу с помощью переключателя. Дело не только в том, есть ли возвращаемое значение.
Если вы собираетесь утверждать, что нет формального исследования в поддержку этого, вам следует ссылаться на то, что противоречит этому.
Мердад
Mehrdad, если есть формальное исследование в поддержку этого, покажите это. Вот и все. Настаивание на доказательствах против переносит бремя доказывания.
Энтони
7

Одно возвращение облегчает рефакторинг. Попробуйте выполнить «метод извлечения» во внутреннем теле цикла for, который содержит возврат, разрыв или продолжение. Это не удастся, поскольку вы нарушили поток управления.

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

Часто я сталкиваюсь с проблемой, что мне нужно полностью переформулировать функции, если они содержат прерыватели потока управления и если я хочу добавить лишь небольшую функциональность. Это очень подвержено ошибкам, так как вы меняете весь поток управления вместо того, чтобы вводить новые пути в изолированные вложения. Если у вас есть только один возврат в конце или вы используете охрану для выхода из цикла, у вас, конечно, больше вложенности и больше кода. Но вы получаете возможности рефакторинга, поддерживаемые компилятором и IDE.

oopexpert
источник
То же самое относится и к переменным. Которые являются альтернативой использованию конструкций control-flow, таких как ранний возврат.
Дедупликатор
Переменные в основном не будут мешать вам разбить ваш код на части так, чтобы существующий поток управления был сохранен. Попробуйте «извлечь метод». Среды IDE могут выполнять только предварительный рефакторинг потока управления, поскольку они не могут получить семантику из того, что вы написали.
oopexpert
5

Учтите, что множественные операторы возврата эквивалентны наличию GOTO для одного оператора возврата. Это то же самое, что и с операторами break. Таким образом, некоторые, как и я, считают их GOTO для всех намерений и целей.

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

Мое общее правило: GOTO предназначены только для управления потоком. Они никогда не должны использоваться для зацикливания, и вы никогда не должны идти «вверх» или «назад». (как работает перерыв / возврат)

Как уже упоминали другие, необходимо прочитать следующее заявление GOTO, которое считается вредным.
Однако имейте в виду, что оно было написано в 1970 году, когда GOTO слишком злоупотребляли. Не все GOTO вредны, и я не стал бы препятствовать их использованию, если вы не используете их вместо обычных конструкций, а скорее в странном случае, когда использование нормальных конструкций будет крайне неудобно.

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

оборота user606723
источник
6
Все структурированные конструкции, которые заменяют goto, реализуются в терминах goto. Например, циклы «если» и «дело». Это не делает их плохими - фактически наоборот. Также это "намерения и цели".
Энтони
Прикоснись, но это не меняет мою точку зрения ... Это просто делает мои объяснения немного неправильными. Ну что ж.
user606723 10.11.11
GOTO всегда должен быть в порядке, если (1) цель находится в том же методе или функции и (2) направление вперед в коде (пропустите некоторый код) и (3) цель не находится внутри какой-либо другой вложенной структуры (например, Перейдите от середины кейса до середины кейса). Если вы будете следовать этим правилам, все злоупотребления GOTO будут иметь действительно сильный запах кода как визуально, так и логически.
Микко Ранталайнен
3

Цикломатическая Сложность

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

Изменение типа возврата

Многократные возвраты означают, что нам нужно изменить в нескольких местах функции, когда мы решим изменить наш тип возврата.

Множественный выход

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

Рефакторированный раствор

Решение нескольких операторов возврата состоит в том, чтобы заменить их полиморфизмом, имеющим один возврат, после разрешения требуемого объекта реализации.

сортировщик
источник
3
Переход от нескольких возвратов к установке возвращаемого значения в нескольких местах не устраняет цикломатическую сложность, а только объединяет местоположение выхода. Все проблемы, которые цикломатическая сложность может указывать в данном контексте, остаются. «Труднее отлаживать, так как логика должна быть тщательно изучена в сочетании с условными операторами, чтобы понять, что вызвало возвращаемое значение». Опять же, логика не меняется, объединяя возвращаемое значение. Если вам нужно тщательно изучить код, чтобы понять, как он работает, его нужно рефакторинг, полная остановка.
WillD