Звідки взялося поняття "єдиного повернення"?


1055

Я часто розмовляю з програмістами, які говорять " Не кладіть кілька заяв на повернення одним і тим же методом ". Коли я прошу їх сказати мені причини, чому я отримую лише " Стандарт кодування так говорить " або " Це заплутано ". Коли вони показують мені рішення з одним твердженням повернення, код мені виглядає більш неприємним. Наприклад:

if (condition)
   return 42;
else
   return 97;

" Це некрасиво, ви повинні використовувати локальну змінну! "

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

Як це 50-відсоткове розширення коду полегшує розуміння програми? Особисто мені це важче, тому що простір станів просто збільшився ще однією змінною, яку легко можна було б запобігти.

Звичайно, зазвичай я просто напишу:

return (condition) ? 42 : 97;

Але багато програмістів уникають умовного оператора і віддають перевагу довгій формі.

Звідки взялося це поняття "єдиного повернення"? Чи є історична причина, чому ця конвенція виникла?


2
Це дещо пов'язане з рефакторингом застереження про охорону. stackoverflow.com/a/8493256/679340 Застереження про захист додасть повернення до початку ваших методів. І це робить код набагато чистішим на мою думку.
Пьотр Перак

3
Він походить від поняття структурованого програмування. Деякі можуть стверджувати, що наявність лише одного повернення дозволяє легко змінювати код, щоб зробити щось безпосередньо перед поверненням або легко налагоджувати.
мартінкунев

3
Я думаю, що приклад є досить простим випадком, коли я не мав би твердої думки так чи інакше. ідеал "єдиний вхід-єдиний вихід" - це більше для того, щоб відвести нас від шалених ситуацій, як 15 заяв про повернення та ще дві гілки, які взагалі не повертаються!
мендота

2
Це одна з найгірших статей, яку я коли-небудь читав. Схоже, автор витрачає більше часу на фантазію про чистоту свого ООП, ніж насправді на роздуми, як чогось досягти. Дерева виразів та оцінок мають значення, але не тоді, коли ви можете просто написати нормальну функцію.
DeadMG

3
Ви повинні повністю усунути стан. Відповідь - 42.
Камбунбурзький

Відповіді:


1119

"Один запис, одинарний вихід" було написано, коли більшість програмувань проводилися мовою асемблери, 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, з додатковим ускладненням у тому, що умова гілки була не поруч із гілкою, а десь у підпрограмі.


38
І не забудьте код спагетті . Підпрограми не були невідомими для виходу з використанням GOTO замість повернення, залишаючи параметри виклику функції та адресу повернення у стеці. Поодинокий вихід рекламувався як спосіб принаймні послідовно виконувати всі шляхи коду до оператора RETURN.
TMN

2
@TMN: у перші дні більшість машин не мали апаратного стеку. Рекурсія зазвичай не підтримувалася. Аргументи підпрограми та зворотна адреса зберігалися у фіксованих місцях, розташованих поруч з кодом підпрограми. Повернення було просто непрямим гото.
кевін клайн

5
@kevin: Так, але, на вашу думку, це вже навіть не означає, що це було винайдено. (До речі, я фактично обґрунтовано впевнений, що Фред запитав , чи надано перевагу нинішній інтерпретації "Єдиного виходу".) Крім того, у C з'явилося constще до того, як багато хто з користувачів тут народився, тому більше не потрібно капітальних констант. навіть в C. Але Java зберегли всі ті погані старі звички C .
sbi

3
Тож чи винятки порушують цю інтерпретацію Single Exit? (Або їх більш примітивний двоюрідний брат setjmp/longjmp,?)
Мейсон Уілер

2
Незважаючи на те, що ОП запитав про поточну інтерпретацію єдиного повернення, ця відповідь відповідає найбільш історичним корінням. Немає сенсу використовувати єдине повернення як правило , якщо ви не хочете, щоб ваша мова відповідала дивовижності VB (не .NET). Просто пам’ятайте, щоб також використовувати булеву логіку без короткого замикання.
ацелент

912

Це поняття Single Entry, Single Exit (SESE) походить з мов із чітким керуванням ресурсами , як, наприклад, C та зборка. У З таким кодом буде витікати ресурси:

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

У таких мовах у вас є три варіанти:

  • Повторіть код очищення.
    Тьфу. Надлишок завжди поганий.

  • Використовуйте a, 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 робить код складнішим . Це динозавр, який (за винятком С) не добре вписується в більшість сучасних мов. Замість того, щоб допомагати зрозумілості коду, він перешкоджає цьому.


Чому програмісти Java дотримуються цього? Я не знаю, але від моєї (ззовні) POV, Java взяла багато конвенцій від C (де вони мають сенс) і застосувала їх до свого OO світу (де вони марні чи відверто погані), де зараз це дотримується їх, незалежно від витрат. (Як і угода про визначення всіх змінних на початку області.)

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


52
Правильно. У Java код очищення належить до finallyпунктів, де він виконується незалежно від ранніх returns чи виключень.
dan04

15
@ dan04 на Java 7 вам навіть не потрібна finallyбільшість часу.
Р. Мартіньо Фернандес

93
@Steven: Звичайно, ви можете це продемонструвати! Насправді, ви можете показати складний і складний код з будь-якою функцією, яку також можна показати, щоб зробити код простішим і легшим для розуміння. Все можна зловживати. Сенс полягає в тому, щоб написати код так, щоб його було легше зрозуміти , а коли це стосується викидання SESE у вікно, так і нехай, і прокляття старих звичок, які застосовувались до різних мов. Але я б не вагався контролювати виконання змінними, якби я вважав, що це полегшує читання коду. Просто я не можу згадати, як я бачив такий код майже за два десятиліття.
sbi

21
@Karl: Дійсно, такий GC-мов, як Java, сильний недолік, який позбавляє вас від необхідності очищати один ресурс, але не вдається з усіма іншими. (C ++ вирішує цю проблему для всіх ресурсів, що використовують RAII .) Але я навіть не говорив лише про пам’ять (я лише ставлю malloc()та free()в коментар як приклад), я говорив про ресурси в цілому. Я також не мав на увазі, що GC вирішить ці проблеми. (Я згадав C ++, у якого немає GC поза коробкою.) З того, що я розумію, у finallyвирішенні цієї проблеми використовується Java .
sbi

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()і записує результат.


31
Я також додав би, що це робить налагодження простішим, оскільки вам потрібно коли-небудь встановити одну точку розриву, щоб отримати всі виходи * з функції. Я вважаю, що деякі ІДЕ дозволяють вам поставити точку перерви на близьку дужку функції, щоб зробити те саме. (* якщо ви не зателефонуєте на вихід)
Skizz

3
З аналогічної причини це також полегшує розширення (додавання) функції, оскільки ваш новий функціонал не потрібно вставляти перед кожним поверненням. Скажімо, вам потрібно оновити журнал, наприклад, з результатом виклику функції.
JeffSahol

63
Чесно кажучи, якби я підтримував цей код, я вважав за краще мати чітко визначений _function(), з returns у відповідних місцях, і обгортку з назвою, function()яка обробляє сторонні журнали, ніж мати єдиний function()з викривленою логікою, щоб усі повернення вписалися в один вихід -пункт просто, щоб я міг вставити додатковий вислів перед цим пунктом
ruakh

11
У деяких відладчика (MSVS) ви можете поставити точку зупину на останній закриває фігурної дужки
Abyx

6
друк! = налагодження. Це зовсім не аргумент.
Пьотр Перак

53

"Єдиний вхід, єдиний вихід" виник із революцією Структурного програмування початку 1970-х років, яку розпочав лист Едсгера У. Дійкстри до редактора " Заява GOTO вважається шкідливою ". Концепції структурованого програмування були детально викладені в класичній книзі "Структуроване програмування" Оле Йохана-Даля, Едсгера У. Дійкстри та Чарльза Ентоні Річарда Хоара.

"GOTO-заява вважається шкідливою" потрібно прочитати навіть сьогодні. "Структуроване програмування" датоване, але все ще дуже, дуже корисно, і воно повинно бути вгорі у списку "Потрібно читати" будь-якого розробника, набагато вище, ніж, наприклад, Стів МакКоннелл. (У розділі Даля викладені основи занять у Simula 67, які є технічною основою для класів на C ++ та всього об’єктно-орієнтованого програмування.)


6
Стаття була написана за кілька днів до С, коли GOTO використовували значне використання. Вони не ворог, але ця відповідь, безумовно, правильна. Повернення твердження, що не в кінці функції, фактично є goto.
user606723

31
Стаття також була написана в ті часи, коли gotoможна було буквально перейти куди завгодно , як-от прямо в якусь випадкову точку в іншій функції, обійшовши будь-яке поняття про процедури, функції, стек виклику і т. Д. Жодна розумна мова не дозволяє цього дня робити прямо goto. C setjmp/ longjmp- це єдиний напіввиключний випадок, про який я знаю, і навіть це вимагає співпраці з обох цілей. (Напівіронічно, що я там вживав слово "винятковий", хоча, враховуючи, що винятки роблять майже те саме ...) В основному стаття перешкоджає практиці, яка давно померла.
cHao

5
З останнього абзацу "Заява Гото, що вважається шкідливою": "в [2] Гвісеппе Якопіні, здається, доведена (логічна) зайвість заяви" Перехід ". Вправа перекладати довільну діаграму потоків більш-менш механічно в стрибковий менша одна, однак, не рекомендується . Тоді очікувана схема потоку не може бути більш прозорою, ніж початкова ".
hugomg

10
Що це стосується питання? Так, робота Dijkstra врешті призвела до мов SESE, і що ж? Так само і працював Беббідж. І , можливо , ви повинні перечитати папір , якщо ви думаєте , що говорить що - то про наявність кількох точок виходу в функції. Тому що це не так.
jalf

10
@John, ти, схоже, намагаєшся відповісти на питання, насправді не відповідаючи на нього. Це прекрасний список для читання, але ви нічого не цитували і не перефразовували, щоб виправдати свою заяву про те, що цей нарис і книга мають що сказати про занепокоєння запитувача. Дійсно, поза коментарями ви нічого не сказали суттєвого щодо питання. Розгляньте розширення цієї відповіді.
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 із Refactoring ...


11
Ще один поганий приклад: це так само легко можна виправити за допомогою if-ifs.
Джек

1
Ваш приклад нечесний, як щодо цього: подвійний getPayAmount () {double ret = normalPayAmount (); if (_isDead) ret = deadAmount (); якщо (_isSeparated) ret = відокремленийAmount (); if (_isRetired) ret = retiredAmount (); повернути рет; };
Charbel

6
@Charbel Це не те саме. Якщо _isSeparatedі чи _isRetiredможе бути правдою (а чому б це не було можливим?), Ви повернете неправильну суму.
hvd

2
@Konchog " вкладені умовні умови забезпечать кращий час виконання, ніж охоронні пункти " Це головним чином потребує цитування. У мене є сумніви, що це взагалі надійно вірно. Наприклад, наприклад, чим раннє повернення відрізняється від логічного короткого замикання з точки зору генерованого коду? Навіть якби це мало значення, я не можу уявити собі випадок, коли різниця була б більше, ніж нескінченно малий ковзання. Отже, ви застосовуєте передчасну оптимізацію, роблячи код менш читабельним, просто щоб задовольнити деякий недоведений теоретичний момент щодо того, що, на вашу думку, призводить до трохи швидшого коду. Ми цього не робимо
underscore_d

1
@underscore_d, ти маєш рацію. це багато залежить від компілятора, але це може зайняти більше місця. Подивіться на дві псевдосклади, і легко зрозуміти, чому захисні пропозиції надходять з мов високого рівня. "А" тест (1); відділення_файла; тест (2); відділення_файла; тест (3); відділення_файла; {CODE} закінчення: повернення; Тест "В" (1); гілка_добра наступна1; повернення; наступний1: тест (2); гілка_добро наступна2; повернення; наступний2: тест (3); гілка_добро наступна3; повернення; наступний3: {CODE} повернення;
Кончог

11

Я написав повідомлення в блозі на цю тему.

Суть полягає в тому, що це правило походить від віку мов, які не мають сміття чи обробку винятків. Не існує офіційного дослідження, яке б показало, що це правило призводить до кращого кодування в сучасних мовах. Не соромтесь ігнорувати це, коли це призведе до скорочення або більш читаного коду. Хлопці Java, які наполягають на цьому, сліпо і сумнівно дотримуються застарілого, безглуздого правила.

Це питання також було задано на Stackoverflow


Гей, я більше не можу дійти до цього посилання. Чи трапляється у вас версія, яка розміщена десь досі?
Нік Хартлі

Привіт, QPT, гарне місце. Я повернув допис у блозі та оновив вищезгадану URL-адресу. Це має посилатися зараз!
Антоній

В цьому є більше, ніж це. Набагато простіше керувати точними термінами виконання за допомогою SESE. Вкладені кондиціонери часто можна реконструювати за допомогою перемикача. Справа не лише в тому, чи є повернене значення чи ні.
Кончог

Якщо ви збираєтесь стверджувати, що не існує офіційного дослідження на його підтримку, вам було б дозволити посилання на те, що йде проти цього.
Мехрдад

Мехрдад, Якщо є офіційне дослідження на його підтвердження, покажіть це. Це все. Наполягаючи на доказах проти, це перекладає тягар доказування.
Ентоні

7

Одне повернення полегшує рефакторинг. Спробуйте виконати "метод вилучення" до внутрішнього корпусу циклу for, який містить повернення, розрив або продовження. Це не вдасться, оскільки ви порушили контрольний потік.

Справа в тому: я гадаю, що ніхто не претендує на написання ідеального коду. Таким чином, код регулярно піддається рефакторингу, щоб його "покращувати" та розширювати. Тому моєю метою було б зберегти свій код максимально сприятливим для рефакторингу.

Часто я стикаюся з проблемою, що мені доводиться повністю переформулювати функції, якщо вони містять контрольні вимикачі потоку і якщо я хочу додати лише невелику функціональність. Це дуже схильне до помилок, коли ви змінюєте весь потік управління замість того, щоб вводити нові шляхи до ізольованих гнізд. Якщо у вас є лише одне повернення в кінці або якщо ви використовуєте охоронців для виходу з циклу, ви, звичайно, маєте більше гніздування та більше коду. Але ви отримуєте можливості компілятора та IDE, що підтримують рефакторинг.


Те саме стосується змінних. Які є альтернативою використанню конструкцій control-flow, таких як раннє повернення.
Дедуплікатор

Змінні здебільшого не перешкоджатимуть вам розбивати код на частини таким чином, щоб зберегти існуючий потік управління. Спробуйте "метод вилучення". Ідентифікатори IDE можуть виконувати лише попередні перетворення потоку управління, оскільки вони не в змозі отримати семантику з написаного вами.
oopexpert

5

Розглянемо той факт, що кілька операторів повернення еквівалентно наявності GOTO в одній заяві повернення. Це той самий випадок із заявами про перерви. Таким чином, деякі, як я, вважають їх GOTO для всіх намірів та цілей.

Однак я не вважаю ці типи GOTO шкідливими і не вагаюся використовувати фактичний GOTO у своєму коді, якщо знайду для цього вагому причину.

Моє загальне правило: GOTO призначені лише для контролю потоку. Вони ніколи не повинні використовуватися для циклічного циклу, і ви ніколи не повинні GOTO "вгору" або "назад". (як працює перерва / повернення)

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

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


6
Всі структуровані конструкції, які замінюють gotos, реалізовані в термінах goto. Напр. Циклів, "якщо" та "регістр". Це не робить їх поганими - насправді навпаки. Також це "наміри та цілі".
Антоній

Дотик, але це не відрізняється моєю точкою ... Це просто робить моє пояснення трохи неправильним. Ну добре.
user606723

GOTO має бути завжди гаразд, поки (1) ціль знаходиться в межах одного методу або функції, і (2) напрямок вперед в коді (пропустити якийсь код) і (3) ціль не знаходиться всередині якоїсь іншої вкладеної структури (наприклад, GOTO від середини if-case до середини else-case). Якщо ви дотримуєтесь цих правил, усі зловживання GOTO мають дійсно сильний код запаху як візуально, так і логічно.
Мікко Ранталайнен

3

Цикломатична складність

Я бачив, як SonarCube використовує кілька операторів повернення для визначення циклічної складності. Отже, чим більше зворотних тверджень, тим вища цикломатична складність

Зміна типу повернення

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

Багаторазовий вихід

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

Реконструйований розчин

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


3
Перехід від декількох повернень до встановлення значення повернення в декількох місцях не усуває цикломатичної складності, він лише уніфікує місце виходу. Всі проблеми, які цикломатична складність може вказати в даному контексті, залишаються. "Це важче виправити помилку, оскільки логіку потрібно ретельно вивчити разом з умовними твердженнями, щоб зрозуміти, що спричинило повернене значення". Знову ж, логіка не змінюється, об'єднуючи повернення. Якщо вам потрібно уважно вивчити код, щоб зрозуміти, як він працює, його потрібно відремонтувати, повністю зупинити.
WillD
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.