Чи повинна функція мати лише одне твердження повернення?


780

Чи є вагомі причини, чому кращою практикою є лише одне твердження про повернення у функції?

Або добре повертатися з функції, як тільки це логічно правильно, це означає, що у функції може бути багато тверджень про повернення?


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

3
Це тісно пов’язано і має чудові відповіді: programmers.stackexchange.com/questions/118703/…
Тім Шмелтер

мовно-агностичний? Поясніть комусь, хто використовує функціональну мову, що він повинен використовувати одне повернення за функцією: p
Boiethios

Відповіді:


741

У мене часто є кілька тверджень на початку способу повернутися для "легких" ситуацій. Наприклад, це:

public void DoStuff(Foo foo)
{
    if (foo != null)
    {
        ...
    }
}

... можна зробити більш читабельним (IMHO), як це:

public void DoStuff(Foo foo)
{
    if (foo == null) return;

    ...
}

Так, так, я думаю, що нормально мати кілька "точок виходу" з функції / методу.


83
Домовились. Хоча наявність декількох точок виходу може вийти з рук, я, безумовно, думаю, що це краще, ніж помістити всю свою функцію в блок IF. Використовуйте return так часто, як це має сенс, щоб ваш код читався.
Джошуа Кармоді,

172
Це відомо, як "заява охоронця" - це Рефакторинг Фаулера.
Ларс Вестергрен

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

21
Величезний блок тверджень if-else, кожен із поверненням? Це нічого. Така річ зазвичай легко відновлюється. (принаймні, більш поширений варіант одного виходу зі змінною результату, тому я сумніваюся, що варіація мультивиходу буде складніше.) Якщо ви хочете справжнього головного болю, подивіться на комбінацію if-else та циклів циклу (контролюється локальною булеви), де встановлюються змінні результату, що ведуть до кінцевої точки виходу в кінці методу. Це ідея єдиного виходу з глузду, і так, я говорю з фактичного досвіду, маючи з цим впоратися.
Маркус Андрен

7
"Уявіть собі: вам потрібно виконати метод" УвеличитиStuffCallCounter "наприкінці функції" DoStuff ". Що ви зробите в цьому випадку? :) '-DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
Джим Балтер

355

Ніхто не згадував і не цитував Кодекс повний, тому я це зроблю.

17.1 повернення

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

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


64
+1 за нюанс "мінімізувати", але не забороняти багаторазове повернення.
Raedwald

13
"важче зрозуміти" є дуже суб'єктивним, особливо коли автор не надає жодних емпіричних доказів на підтвердження загального твердження ... наявність єдиної змінної, яка повинна бути встановлена ​​у багатьох умовних місцях у коді для остаточного твердження про повернення, однаково піддається "не знаючи про можливість того, що змінна була призначена десь вище, ніж вона функціонує!
Хестон Т. Хольтман

26
Особисто я віддаю перевагу поверненню рано, де це можливо. Чому? Що ж, коли ви бачите ключове слово повернення для даного випадку, ви відразу знаєте, що "я закінчив" - вам не доведеться читати, щоб зрозуміти, що, якщо що, відбувається згодом.
Марк Сімпсон

12
@ HestonT.Holtmann: Справа в тому, що зробив Досконалий код унікальний серед книг по програмуванню в тому , що рада є підкріплені емпіричними даними.
Адріан Маккарті

9
Це, мабуть, має бути прийнятою відповіддю, оскільки в ній зазначається, що наявність декількох точок повернення не завжди є хорошою, але іноді необхідною.
Рафід

229

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

string fooBar(string s, int? i) {
  string ret = "";
  if(!string.IsNullOrEmpty(s) && i != null) {
    var res = someFunction(s, i);

    bool passed = true;
    foreach(var r in res) {
      if(!r.Passed) {
        passed = false;
        break;
      }
    }

    if(passed) {
      // Rest of code...
    }
  }

  return ret;
}

Порівняйте це з кодом , де кілька точок виходу є дозволеними: -

string fooBar(string s, int? i) {
  var ret = "";
  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

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


12
Чіткість в очах глядача - я дивлюся на функцію і шукаю початку середини та кінця. Коли функція невелика, її добре, але коли ви намагаєтеся розібратися, чому щось порушено, і "Решта коду" виявляється нетривіальним, ви можете витратити віки на пошуки того, чому рет
Мерф

7
Перш за все, це надуманий приклад. По-друге, звідки: string ret; "перехід у другій версії? По-третє, Ret не містить корисної інформації. По-четверте, чому так багато логіки в одній функції / методу? По-п’яте, чому б не зробити DidValuesPass (тип res), а потім RestOfCode () роздільним підфункції?
Рік Мінеріх

25
@ Rick 1. Не схильний до мого досвіду, це насправді модель, яку я зустрічав багато разів. 3. Гм? Це приклад? 4. Добре, мабуть, це надумано в цьому відношенні, але це можна виправдати дуже багато, 5. міг би зробити ...
ljs

5
@ Справа в тому, що часто простіше повернутися рано, ніж загортати код у величезний вислів if. З мого досвіду це багато чого, навіть при належному рефакторингу.
ljs

5
Справа в тому, щоб не мати декількох операторів повернення, - це налагодження налагодження простіше, далеко за секунду - для читабельності. Одна точка розриву в кінці дозволяє побачити значення виходу, не враховуючи винятків. Що стосується читабельності, то 2 показані функції ведуть себе не однаково. Повернення за замовчуванням - це порожній рядок, якщо! R.Passed, але "більш читабельний" змінює це, щоб повернути null. Автор неправильно прочитав, що раніше був за замовчуванням лише через кілька рядків. Навіть у тривіальних прикладах легко мати незрозумілі повернення за замовчуванням, щось, що сприяє виконанню одного повернення в кінці.
Мітч

191

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

Маючи теорію "єдиної точки виходу", ви неминуче закінчуєтесь кодом, який виглядає приблизно так:

function()
{
    HRESULT error = S_OK;

    if(SUCCEEDED(Operation1()))
    {
        if(SUCCEEDED(Operation2()))
        {
            if(SUCCEEDED(Operation3()))
            {
                if(SUCCEEDED(Operation4()))
                {
                }
                else
                {
                    error = OPERATION4FAILED;
                }
            }
            else
            {
                error = OPERATION3FAILED;
            }
        }
        else
        {
            error = OPERATION2FAILED;
        }
    }
    else
    {
        error = OPERATION1FAILED;
    }

    return error;
}

Це не лише робить цей код дуже важким для дотримання, але тепер скажіть, що пізніше вам потрібно повернутися назад і додати операцію між 1 і 2. Ви повинні відступити лише про всю вигадливу функцію, і удача, переконавшись у тому, що всі ваші умови if та else підбираються належним чином.

Цей метод робить обслуговування коду надзвичайно складним та схильним до помилок.


5
@Murph: Ви не можете знати, що більше нічого не відбувається після кожної умови, не читаючи кожного. Зазвичай я б сказав, що такі теми є суб'єктивними, але це явно неправильно. З віддачею за кожну помилку, ви закінчили, ви точно знаєте, що сталося.
ГЕОЧЕТ

6
@Murph: Я бачив такий код, який використовувався, зловживав і використовував. Приклад досить простий, оскільки всередині немає істини, якщо / ще. Все, що потрібен для розбиття цього коду, - це ще одне «забуте». AFAIK, цей код справді дуже потребує винятків.
paercebal

15
Ви можете перефактурувати його на це, зберігаючи його "чистоту": if (! SUCCEEDED (Operation1 ())) {} else error = OPERATION1FAILED; if (помилка! = S_OK) {if (SUCCEEDED (Operation2 ())) {} else error = OPERATION2FAILED; } if (помилка! = S_OK) {if (SUCCEEDED (Operation3 ())) {} else error = OPERATION3FAILED; } // тощо.
Джо Пінеда

6
Цей код не має лише однієї точки виходу: кожне з цих "error =" тверджень знаходиться на шляху виходу. Йдеться не лише про вихід функцій, це про вихід із будь-якого блоку чи послідовності.
Джей Базузі

6
Я не погоджуюся, що одне повернення "неминуче" призводить до глибокого гніздування. Ваш приклад можна записати як просту лінійну функцію з єдиним поверненням (без gotos). І якщо ви не можете або не можете розраховувати виключно на RAII для управління ресурсами, то раннє повернення в кінцевому підсумку призводить до витоків або дублювання коду. Найголовніше, що раннє повернення робить непрактичним твердження про умови.
Адріан Маккарті

72

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


44
Структуроване програмування нічого не говорить. Про це говорять деякі (але не всі) люди, які описують себе як прихильники структурованого програмування.
wnoise

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

6
@wnoise +1 для коментаря, так правда. Усі "структурні програми" говорять, що не використовуйте GOTO.
paxos1977

1
@ceretullis: якщо це не потрібно. Звичайно, це не суттєво, але може бути корисним у C. Ядро Linux використовує його і з поважних причин. GOTO вважає шкідливим говорити про використання GOTOдля переміщення потоку управління навіть тоді, коли функція існувала. Ніколи не сказано "ніколи не використовуй GOTO".
Естебан Кюбер

1
"все стосуються ігнорування" структури "коду." - Ні, зовсім навпаки. "є сенс сказати, що їх слід уникати" - ні, це не так.
Джим Балтер

62

Як зазначає Кент Бек, обговорюючи положення про охорону в "Шаблонах впровадження", що створюють звичайну процедуру, мають єдину точку входу та виходу ...

"було запобігати плутанині, можливій під час стрибків у та з багатьох місць в одному розпорядку. Це було добре, коли застосовувались до FORTRAN або мовних програм мовлення, написаних з великою кількістю глобальних даних, де навіть розуміння, які заяви виконувались, було важкою роботою." "Малі методи та здебільшого локальні дані є непотрібними".

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


Звичайно, "один довгий вкладений набір тверджень if-then-else" - не єдина альтернатива захисних статей.
Адріан Маккарті

@AdrianMcCarthy У вас є краща альтернатива? Це було б корисніше сарказму.
shinzou

@kuhaku: Я не впевнений, що назвав би цей сарказм. Відповідь говорить про те, що це / або ситуація: або охоронні пункти, або довгі вкладені пучки if-then-else. Багато (більшість?) Мов програмування пропонують багато способів врахування такої логіки, окрім запобіжних положень.
Адріан Маккарті

61

У функції, яка не має побічних ефектів, немає вагомих причин мати більше, ніж одне повернення, і ви повинні писати їх у функціональному стилі. У методі з побічними ефектами все є більш послідовним (індексованим часом), тому ви пишете в імперативному стилі, використовуючи оператор return як команду для припинення виконання.

Іншими словами, коли це можливо, надайте перевагу цьому стилю

return a > 0 ?
  positively(a):
  negatively(a);

над цим

if (a > 0)
  return positively(a);
else
  return negatively(a);

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

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


6
+1 "Якщо ви виявите, що ваші ifs та elses синтаксично сильно розташовані, ви можете розбити це на більш дрібні функції."
Андрес Яан Так

4
+1, якщо це проблема, це зазвичай означає, що ви занадто багато робите в одній функції. Мене справді пригнічує, що це не найвища відповідь
Метт Бріггс

1
Заяви охоронців також не мають жодних побічних ефектів, але більшість людей вважають їх корисними. Тому можуть бути причини припинити виконання достроково, навіть якщо побічних ефектів немає. На мою думку, ця відповідь не повністю вирішує це питання.
Maarten Bodewes

@ MaartenBodewes-Owlstead Дивіться "Строгість і
лінь

43

Я бачив це в стандартах кодування для C ++, які перестали працювати з C, як якщо б у вас немає RAII або іншого автоматичного управління пам'яттю, тоді вам потрібно очищати кожне повернення, що означає "вирізати і вставити" очищення або гото (логічно те саме, що "нарешті" в керованих мовах), обидва вони вважаються поганою формою. Якщо ваша практика полягає у використанні розумних покажчиків та колекцій у C ++ або іншій автоматичній системі пам’яті, то для цього немає вагомих причин, і це стає все для читабельності та більше виклику судження.


Добре сказано, хоча я вважаю, що краще копіювати видалених під час спроби написання високооптимізованого коду (наприклад, програмного забезпечення, що знімає складні 3d-сітки!)
Грант Петерс,

1
Що змушує вас повірити в це? Якщо у вас є компілятор з поганою оптимізацією, де є деяка накладні витрати на перенаправлення auto_ptr, ви можете паралельно використовувати звичайні покажчики. Хоча було б дивно писати «оптимізований» код з неоптимізуючим компілятором в першу чергу.
Піт Кіркхем

Це робить цікавим винятком із правила: Якщо ваша мова програмування не містить те, що автоматично викликається в кінці методу (наприклад, try... finallyна Java), і вам потрібно виконати обслуговування ресурсів, яке ви могли б зробити з одним повернення в кінці методу. Перш ніж зробити це, слід серйозно подумати про рефакторинг коду, щоб позбутися від ситуації.
Maarten Bodewes

@PeteKirkham чому погано йти на очищення? так, Goto може використовуватися погано, але саме це використання не є поганим.
q126y

1
@ q126y в C ++, на відміну від RAII, він не працює, коли викидається виняток. В C це цілком справедлива практика. Дивіться stackoverflow.com/questions/379172/use-goto-or-not
Піт Кіркем

40

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


38

Чи є вагомі причини, чому кращою практикою є лише одне твердження про повернення у функції?

Так , є:

  • Єдина точка виходу дає чудове місце для затвердження ваших пост-умов.
  • Можливість поставити точку розриву налагоджувача на одне повернення в кінці функції часто корисно.
  • Менше повернення означає меншу складність. Лінійний код, як правило, простіший для розуміння.
  • Якщо спроба спростити функцію до єдиного повернення викликає складність, то це стимул перетворювати на менші, більш загальні, простіші для розуміння функції.
  • Якщо ви розмовляєте мовою без деструкторів або не використовуєте RAII, то одноразове повернення зменшує кількість місць, які вам доведеться прибрати.
  • Для деяких мов потрібна одна точка виходу (наприклад, Паскаль та Ейфелева).

Питання часто задається помилковою дихотомією між декількома поверненнями або глибоко вкладеними у висловлюваннях. Майже завжди є третє рішення, яке є дуже лінійним (без глибокого гніздування) лише з однією точкою виходу.

Оновлення : Очевидно, вказівки MISRA також сприяють одному виходу .

Щоб було зрозуміло, я не кажу, що завжди неправильно мати кілька повернень. Але з огляду на рівнозначні рішення, є багато вагомих причин віддати перевагу тому, що має єдину віддачу.


2
Ще одна вагома причина, мабуть, найкраща в ці дні, щоб мати єдину заяву про повернення - це реєстрація. якщо ви хочете додати журнал до методу, ви можете розмістити єдиний оператор журналу, який передає те, що метод повертає.
inor

Наскільки поширеною була заява FORTRAN ENTRY? Див. Docs.oracle.com/cd/E19957-01/805-4939/6j4m0vn99/index.html . А якщо ви прихильниці, ви можете ввійти в систему методами AOP та попередньою порадою
Eric Jablow

1
+1 Перших 2 балів було достатньо, щоб переконати мене. Що з другим останнім абзацом. Я б не погоджувався з елементом реєстрації з тієї ж причини, що я відмовляю від глибоких вкладених умов, оскільки вони спонукають порушити правило єдиної відповідальності, яке є основною причиною введення поліморфізму в ООП.
Френсіс Роджерс

Я хотів би лише додати, що для C # та кодових контрактів проблема після умови - це непередача, оскільки ви все ще можете використовувати Contract.Ensuresз декількома точками повернення.
julealgon

1
@ q126y: Якщо ви використовуєте gotoдля переходу до загального коду очищення, ви, ймовірно, спростили функцію, щоб returnв кінці коду очищення був один. Отже, ви можете сказати, що ви вирішили проблему goto, але я б сказав, що ви вирішили її, спростивши до єдиної return.
Адріан Маккарті

33

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


6
БРАВО! Ви єдина людина, яка згадує цю об’єктивну причину. Тому я віддаю перевагу одній точці виходу проти кількох точок виходу. Якби мій налагоджувач міг встановити точку розриву на будь-якій точці виходу, я, мабуть, вважаю за краще кілька точок виходу. Моя поточна думка - люди, які кодують кілька точок виходу, роблять це на власну користь за рахунок тих, хто повинен використовувати налагоджувач у своєму коді (і так, я кажу про всіх ваших учасників з відкритим кодом, які пишуть код з кілька точок виходу.)
MikeSchinkel

3
ТАК. Я додаю логічний код до системи, яка періодично погано поводиться у виробництві (де я не можу переступити). Якби попередній кодер використовував один вихід, це було б МНОГО простіше.
Майкл Блекберн

3
Правда, у налагодженні це корисно. Але на практиці мені вдалося в більшості випадків встановити точку перерви у функції виклику відразу після дзвінка - ефективно до того ж результату. (І, звичайно, ця позиція знаходиться у стеку викликів.) YMMV.
foo

Це, якщо ваш налагоджувач не забезпечує функцію виходу або повернення кроку (і кожен налагоджувач виконує, наскільки я знаю), який показує повернене значення відразу після повернення. Зміна значення після цього може бути дещо складним, якщо воно не присвоєно змінній.
Maarten Bodewes

7
Я довго не бачив налагоджувача, що не дозволило б вам встановити точку розриву на методі "закриття" методу (кінець, правильна фігурна дужка, незалежно від вашої мови) і натиснути цю точку розриву незалежно від того, де чи скільки , метод повернення є в методі. Крім того, навіть якщо функція має лише одне повернення, це не означає, що ви не можете вийти з функції за винятком (явний або успадкований). Отже, я думаю, що це насправді не є дійсним моментом.
Скотт Гартнер

19

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


"Взагалі я намагаюся мати лише одну точку виходу з функції" - чому? "мета повинна бути якомога меншою кількістю вихідних точок" - чому? І чому 19 людей проголосували за цю невідповідь?
Джим Балтер

@JimBalter Зрештою, це зводиться до особистих уподобань. Більше точок виходу зазвичай призводить до більш складного методу (хоча і не завжди) і ускладнює комусь зрозуміти.
Скотт Дорман

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

14

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

(Цілком крім того, що будь-яка багаторядкова функція на мові з винятками все одно матиме декілька точок виходу.)


14

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

public void DoStuff(Foo foo)
{
    if (foo == null) return;
}

Побачивши цей код, я одразу попрошу:

  • Чи "foo" колись недійсний?
  • Якщо так, то скільки клієнтів "DoStuff" коли-небудь називають функцію з нульовим "foo"?

Залежно від відповідей на ці запитання, це може бути саме так

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

В обох вищезазначених випадках код, ймовірно, може бути перероблений із твердженням, щоб переконатися, що "foo" ніколи не буває нульовим, а відповідні абоненти змінені.

Є дві інші причини (конкретні, я думаю, для коду С ++), коли існує декілька, насправді можуть мати негативний вплив. Вони мають розмір коду та оптимізацію компілятора.

Об'єкт, який не є POD C ++ в області дії на виході з функції, матиме назву деструктора. Якщо є кілька тверджень про повернення, можливо, в обсягах існують різні об'єкти, тому список деструкторів, які потрібно викликати, буде різним. Тому компілятору необхідно генерувати код для кожного оператора return:

void foo (int i, int j) {
  A a;
  if (i > 0) {
     B b;
     return ;   // Call dtor for 'b' followed by 'a'
  }
  if (i == j) {
     C c;
     B b;
     return ;   // Call dtor for 'b', 'c' and then 'a'
  }
  return 'a'    // Call dtor for 'a'
}

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

Інший випуск стосується "Оптимізації іменованого повернення значення" (він же Copy Elision, ISO C ++ '03 12.8 / 15). C ++ дозволяє програмі пропустити виклик конструктора копій, якщо він може:

A foo () {
  A a1;
  // do something
  return a1;
}

void bar () {
  A a2 ( foo() );
}

Просто приймаючи код таким, який є, об'єкт 'a1' будується у 'foo', і тоді його конструкція копіювання буде викликана для побудови 'a2'. Однак копія elision дозволяє компілятору сконструювати "a1" там же, що знаходиться на стеку, як "a2". Тому немає необхідності "копіювати" об'єкт, коли функція повертається.

Кілька точок виходу ускладнює роботу компілятора в спробі виявити це, і принаймні для відносно недавньої версії VC ++ оптимізація не відбулася там, де тіло функції мало кілька повернень. Докладніші відомості див. У розділі Оптимізація іменованого значення повернення у Visual C ++ 2005 .


1
Якщо ви виймете з прикладу C ++ все, окрім останнього dtor, код для знищення B та пізніших C і B все одно доведеться генерувати тоді, коли сфера оператора if закінчується, тому ви дійсно нічого не отримуєте, не маючи декількох повернень .
Затемнення

4
+1 І внизу списку ми маємо НАДІЙНУ причину, що існує така практика кодування - NRVO. Однак це мікрооптимізація; і, як і всі методи мікрооптимізації, був, ймовірно, розпочатий якимось 50-річним "експертом", який звик програмувати на PDP-8 частотою 300 кГц і не розуміє важливості чистого та структурованого коду. Загалом, дотримуйтесь порад Кріса S і використовуйте кілька заяв про повернення, коли це має сенс.
BlueRaja - Danny Pflughoeft

Хоча я не погоджуюся з вашими уподобаннями (на мою думку, ваша пропозиція Assert також є зворотною точкою, як це є throw new ArgumentNullException()в C # в даному випадку), мені дуже сподобалися ваші інші міркування, вони для мене справедливі і можуть бути критичними в деяких нішеві контексти.
julealgon

Це коктейль, повний солом’яників. Питання, чому fooтестують, не має нічого спільного з темою, що потрібно робити, if (foo == NULL) return; dowork; абоif (foo != NULL) { dowork; }
Джим Балтер

11

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


Дуже проникливий. Хоча я відчуваю, що поки програміст не знає, коли використовувати кілька точок виходу, їх слід обмежувати одним.
Рік Мінерих

5
Не зовсім. Цикломатична складність "if (...) return; ... return;" те саме, що "if (...) {...} return;". Вони обоє мають дві стежки через них.
Стів Еммерсон

11

Я змушую себе використовувати лише одне returnтвердження, оскільки це в певному сенсі породжує запах коду. Дозволь пояснити:

function isCorrect($param1, $param2, $param3) {
    $toret = false;
    if ($param1 != $param2) {
        if ($param1 == ($param3 * 2)) {
            if ($param2 == ($param3 / 3)) {
                $toret = true;
            } else {
                $error = 'Error 3';
            }
        } else {
            $error = 'Error 2';
        }
    } else {
        $error = 'Error 1';
    }
    return $toret;
}

(Умови арбітражні ...)

Чим більше умов, тим більше функція отримує, тим складніше її читати. Тож якщо ви знайомі з запахом коду, ви зрозумієте це та захочете змінити код. Два можливих рішення:

  • Кілька прибутків
  • Рефакторинг на окремі функції

Кілька повернень

function isCorrect($param1, $param2, $param3) {
    if ($param1 == $param2)       { $error = 'Error 1'; return false; }
    if ($param1 != ($param3 * 2)) { $error = 'Error 2'; return false; }
    if ($param2 != ($param3 / 3)) { $error = 'Error 3'; return false; }
    return true;
}

Окремі функції

function isEqual($param1, $param2) {
    return $param1 == $param2;
}

function isDouble($param1, $param2) {
    return $param1 == ($param2 * 2);
}

function isThird($param1, $param2) {
    return $param1 == ($param2 / 3);
}

function isCorrect($param1, $param2, $param3) {
    return !isEqual($param1, $param2)
        && isDouble($param1, $param3)
        && isThird($param2, $param3);
}

Зрозуміло, вона довша і трохи безладна, але в процесі рефакторингу функції таким чином ми маємо

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

5
-1: Поганий приклад. Ви пропустили обробку повідомлення про помилку. Якщо цього не потрібно, isCorrect може бути виражений так само, як return xx && yy && zz; де xx, yy і z є рівними, isDouble і isThird виразами.
кауппі

10

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

Я особисто ніколи не чув / не бачив, щоб якісь "найкращі практики" говорили про те, що ви повинні мати лише одне твердження про повернення.

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


10

Я вважаю, що декілька повернень зазвичай хороші (в коді, який я пишу в C #). Стиль єдиного повернення є перехопленням від C. Але ви, мабуть, не кодуєте C.

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

Більше одного стилю повернення може бути шкідливою звичкою в коді С, де ресурси мають бути явно розмежовані, але такі мови, як Java, C #, Python або JavaScript, мають такі конструкції, як автоматичне збирання сміття та try..finallyблоки (іusing блоки в C # ), і цей аргумент не застосовується - у цих мовах дуже рідко потрібна централізована ручна розробка ресурсів.

Бувають випадки, коли одне повернення є більш читабельним, і випадки, коли його немає. Перевірте, чи зменшує вона кількість рядків коду, робить логіку зрозумілішою чи зменшує кількість дужок та відступів або тимчасових змінних.

Тому використовуйте стільки повернень, скільки відповідає вашим художнім почуттям, адже це питання компонування та читабельності, а не технічна.

Я говорив про це більш детально у своєму блозі .


10

Про те, що єдина точка виходу, можна сказати так само, як і погані речі про неминуче програмування «стрілки» .

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

У статті про спартанське програмування "SSDSLPedia" та статті про єдину функцію виходу "Вікісховища Портлендського візерунка" є деякі проникливі аргументи щодо цього. Також, звичайно, є ця посада для розгляду.

Якщо ви дійсно хочете отримати єдину точку виходу (будь-якою мовою, що не включає виняток), наприклад, щоб вивільнити ресурси в одному місці, я вважаю, що ретельне застосування goto є гарним; див. для прикладу цей досить надуманий приклад (стислий для збереження екранного нерухомості):

int f(int y) {
    int value = -1;
    void *data = NULL;

    if (y < 0)
        goto clean;

    if ((data = malloc(123)) == NULL)
        goto clean;

    /* More code */

    value = 1;
clean:
   free(data);
   return value;
}

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

Хоча, роблячи це, я все-таки виявляю кілька точок виходу, як у цьому прикладі, де якась більша функція була розбита на кілька менших функцій:

int g(int y) {
  value = 0;

  if ((value = g0(y, value)) == -1)
    return -1;

  if ((value = g1(y, value)) == -1)
    return -1;

  return g2(y, value);
}

Залежно від проекту або правил кодування, більшість кодів пластини котла можна замінити макросами. Як бічна примітка, розбивши її таким чином, функції g0, g1, g2 дуже легко перевірити індивідуально.

Очевидно, що в мові з підтримкою OO та з винятком я б не використовував такі оператори if (або взагалі, якби я міг уникнути цього з невеликими зусиллями), і код був би набагато простішим. І не стріли. І більшість неофіційних доходів, ймовірно, будуть винятками.

Коротко;

  • Мало повернень краще, ніж багато повернень
  • Більше однієї віддачі краще, ніж величезні стрілки, і охоронні пропозиції, як правило, добре.
  • Винятки могли / повинні, мабуть, замінити більшість "охоронних положень", коли це можливо.

Приклад виходить з ладу для y <0, тому що він намагається звільнити вказівник NULL ;-)
Еріх Кітцмуеллер

2
opengroup.org/onlinepubs/009695399/functions/free.html "Якщо ptr - це нульовий вказівник, жодних дій не повинно бути."
Генрік Густафссон

1
Ні, це не вийде з ладу, тому що передача NULL у вільний - це визначена неоперація. Це прикро поширене помилкове уявлення, яке вам доведеться спершу перевірити на NULL.
hlovdal

Шаблон «стрілки» не є неминучою альтернативою. Це помилкова дихотомія.
Адріан Маккарті

9

Ви знаєте прислів’я - краса в очах глядача .

Деякі люди клянуться NetBeans, а деякі - IntelliJ IDEA , деякі - Python, а деякі - PHP .

У деяких магазинах ви можете втратити роботу, якщо наполягаєте на цьому:

public void hello()
{
   if (....)
   {
      ....
   }
}

Питання стосується наочності та ремонтопридатності.

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

Ей, приятель (як колишній клієнт говорив) роби те, що ти хочеш, доки ти знаєш, як це виправити, коли мені потрібно, щоб ти це полагодив.

Я пам’ятаю 20 років тому, коли мою колегу звільнили за те, що вона сьогодні називатиметься спритною стратегією розвитку . Він мав прискіпливий інкрементальний план. Але його менеджер кричав на нього: "Ви не можете поступово випускати функції користувачам! Ви повинні дотримуватися водоспаду ". Його відповідь менеджеру полягала в тому, що поступовий розвиток буде більш точним до потреб замовника. Він вірив у розробку для потреб клієнтів, але менеджер вірив у кодування до "вимог замовника".

Ми часто винні в порушенні нормалізації даних, меж MVP та MVC . Ми вставляємо замість побудови функції. Ми беремо ярлики.

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

якість = точність, ремонтопридатність та рентабельність.

Усі інші правила відходять на другий план. І звичайно це правило ніколи не згасає:

Лінь - чеснота хорошого програміста.


1
"Ей, приятель (як колишній клієнт говорив) робіть те, що ви хочете, доки ви знаєте, як це виправити, коли мені потрібно, щоб ви виправити це". Проблема: часто це не виправляєте саме ви.
Dan Barron

+1 на цю відповідь, тому що я згоден з тим, куди ви їдете, але не обов'язково, як ви туди потрапите. Я б заперечував, що є рівні розуміння. тобто розуміння того, що працівник A має 5 років досвіду програмування та 5 років роботи з компанією, сильно відрізняється, ніж у співробітника B, нового випускника коледжу, який тільки починається з компанії. Я мою думку про те, що якщо працівник А - це єдиний, хто може зрозуміти код, це НЕ є ремонтом, і тому ми всі повинні прагнути написати код, який може зрозуміти працівник Б. Тут знаходиться справжнє мистецтво в програмному забезпеченні.
Френсіс Роджерс

9

Я схиляюся до використання охоронних пропозицій, щоб повернутися достроково і в іншому випадку вийти в кінці методу. Правило єдиного входу та виходу має історичне значення і було особливо корисним при роботі зі застарілим кодом, який займав до 10 сторінок формату А4 для одного методу C ++ з декількома поверненнями (і безліччю дефектів). З недавніх пір загальноприйнятою хорошою практикою є збереження малих методів, що робить кілька виходів меншими перешкодами для розуміння. У наступному прикладі Kronoz, скопійованому зверху, питання полягає в тому, що відбувається в // Rest of code ... ?:

void string fooBar(string s, int? i) {

  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Я усвідомлюю, що приклад дещо надуманий, але мені сподобається переробити цикл foreach у висловлювання LINQ, яке потім може вважатися захисним застереженням. Знову ж , у надуманий приклад намір коду не є очевидним і SomeFunction () може мати будь - якої іншої побічний ефект або результат може бути використаний в // Решта код ... .

if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;

Надання наступної реконструйованої функції:

void string fooBar(string s, int? i) {

  if (string.IsNullOrEmpty(s) || i == null) return null;
  if (someFunction(s, i).Any(r => !r.Passed)) return null;

  // Rest of code...

  return ret;
}

Чи не має C ++ винятків? Тоді чому б ти повертався nullзамість того, щоб кидати виняток із зазначенням, що аргумент не прийнятий?
Maarten Bodewes

1
Як я зазначив, приклад коду скопійовано з попередньої відповіді ( stackoverflow.com/a/36729/132599 ). Оригінальний приклад повернув нулі та рефакторинг, щоб викинути аргументи з винятками, не був суттєвим для того, що я намагався зробити, або для початкового питання. Що стосується належної практики, тоді так, як правило, я (в C #) кидаю ArgumentNullException у захисний пункт, а не повертаю нульове значення.
Девід Кларк

7

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

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


Це насправді не має значення. Якщо ви зміните тип локальної змінної, яка повертається, вам доведеться виправити всі призначення до цієї локальної змінної. І, мабуть, краще все-таки визначити метод з іншою підписом, оскільки вам доведеться виправити всі виклики методу.
Maarten Bodewes

@ MaartenBodewes-Owlstead - Це може змінити значення. Назвіть лише два приклади, де ви б хотіли не доведеться виправляти всі призначення локальній змінній або змінювати виклики методу, функція може повернути дату у вигляді рядка (локальна змінна буде фактичною датою, відформатована як рядок лише у останній момент), або він може повернути десяткове число, і ви хочете змінити кількість десяткових знаків.
nnnnnn

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

7

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

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


6

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


Не кажучи вже про те, що потрібно говорити пропустити ifзаяву перед кожним викликом методу, який повертає успіх чи ні :(
Maarten Bodewes

6

Одинарна точка виходу - всі інші речі рівні - робить код значно читабельнішим. Але є улов: популярне будівництво

resulttype res;
if if if...
return res;

є підробкою, "res =" не набагато краще, ніж "повернення". У ньому є одне твердження про повернення, але кілька точок, де функція фактично закінчується.

Якщо у вас функція з декількома поверненнями (або "res =" s), часто корисно розбити її на кілька менших функцій з однією точкою виходу.


6

Моя звичайна політика полягає в тому, щоб в кінці функції було лише одне твердження повернення, якщо складність коду значно не зменшується додаванням більше. Насправді я скоріше шанувальник Ейфеля, який застосовує єдине правило повернення, не маючи заяви про повернення (для створення результату є лише створена автоматично автоматична змінна "результат").

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


5

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

Perl 6: поганий приклад

sub Int_to_String( Int i ){
  given( i ){
    when 0 { return "zero" }
    when 1 { return "one" }
    when 2 { return "two" }
    when 3 { return "three" }
    when 4 { return "four" }
    ...
    default { return undef }
  }
}

краще писати так

Перл 6: Хороший приклад

@Int_to_String = qw{
  zero
  one
  two
  three
  four
  ...
}
sub Int_to_String( Int i ){
  return undef if i < 0;
  return undef unless i < @Int_to_String.length;
  return @Int_to_String[i]
}

Зауважте, це був лише швидкий приклад


Добре Чому це було проголосовано? Це не так, як це не думка.
Бред Гілберт

5

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

void ProcessMyFile (char *szFileName)
{
   FILE *fp = NULL;
   char *pbyBuffer = NULL:

   do {

      fp = fopen (szFileName, "r");

      if (NULL == fp) {

         break;
      }

      pbyBuffer = malloc (__SOME__SIZE___);

      if (NULL == pbyBuffer) {

         break;
      }

      /*** Do some processing with file ***/

   } while (0);

   if (pbyBuffer) {

      free (pbyBuffer);
   }

   if (fp) {

      fclose (fp);
   }
}

Ви голосуєте за єдине повернення - в коді С. Але що робити, якщо ви кодували мовою, в якій було зібрано сміття, і спробуйте .. остаточно блокує?
Ентоні

4

Це, мабуть, незвична перспектива, але я вважаю, що тому, хто вважає, що потрібно віддавати кілька заяв про повернення, ніколи не доводилося використовувати налагоджувач на мікропроцесорі, який підтримує лише 4 апаратних точки прориву. ;-)

Хоча проблеми "коду стрілки" повністю коректні, одна проблема, яка, здається, усунеться при використанні декількох операторів повернення, знаходиться в ситуації, коли ви використовуєте відладчик. Ви не маєте зручного положення для лову, щоб поставити точку перелому, щоб гарантувати, що ви збираєтесь побачити вихід, а отже, і стан повернення.


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

Залежить і від вашого налагоджувача.
Maarten Bodewes

4

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

Але, ні, в одному / безлічі зворотних заяв немає нічого поганого. У деяких мовах це краща практика (C ++), ніж в інших (C).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.