it-swarm-eu.dev

Odkud pocházel pojem „pouze jeden návrat“?

Často mluvím s programátory, kteří říkají „Nevkládejte více příkazů pro vrácení stejným způsobem." Když je požádám, aby mi sdělili důvody, proč všechno, co dostanu, je "kódování standard to říká. "nebo" Je to matoucí. "Když mi ukážou řešení s jediným návratovým příkazem, kód mi připadá ošklivější. Například:

if (condition)
   return 42;
else
   return 97;

"To je ošklivé, musíte použít lokální proměnnou!"

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

Jak tento 50% nadýmání kódu usnadňuje pochopení programu? Osobně to považuji za těžší, protože stavový prostor se právě zvětšil o další proměnnou, které bylo možné snadno zabránit.

Samozřejmě bych normálně jen napsal:

return (condition) ? 42 : 97;

Ale mnoho programátorů se vyhýbá podmíněnému operátorovi a dává přednost dlouhé formě.

Odkud pocházel tento pojem „pouze jeden návrat“? Existuje historický důvod, proč tato úmluva vznikla?

1077
fredoverflow

„Single Entry, Single Exit“ bylo napsáno, když byla většina programování prováděna v jazyce Assembly, FORTRAN nebo COBOL. Bylo to široce nesprávně vyloženo, protože moderní jazyky nepodporují postupy, které Dijkstra varoval.

„Jednotný zápis“ znamená „nevytvářejí alternativní vstupní body pro funkce“. V jazyce Shromáždění je samozřejmě možné zadat funkci při jakékoli instrukci. FORTRAN podporoval více záznamů funkcí pomocí příkazu 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)

"Single Exit" znamenal, že funkce by se měla vrátit pouze na jedno místo: výpis bezprostředně následující po volání. Znamenalo to ne, že funkce by měla vrátit pouze z jedno místo. Když bylo napsáno strukturované programování , bylo běžnou praxí, že funkce indikovala chybu návratem na alternativní umístění. FORTRAN to podporoval prostřednictvím „alternativního návratu“:

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'

Obě tyto techniky byly velmi náchylné k chybám. Použití alternativních položek často ponechalo některé proměnné neinicializované. Použití alternativních návratů mělo všechny problémy s příkazem GOTO, s další komplikací, že podmínka větve nesousedila s větví, ale někde v podprogramu.

1145
kevin cline

Tento pojem Single Entry, Single Exit (SESE) pochází z jazyky s explicitním řízením zdrojů, jako C a shromáždění. V C takový kód uvolní zdroje:

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

V takových jazycích máte v zásadě tři možnosti:

  • Replikujte čisticí kód.
    Ugh. Redundance je vždy špatná.

  • Pomocí goto přejděte na kód vyčištění.
    To vyžaduje, aby byl kód vyčištění poslední věcí ve funkci. (A to je důvod, proč někteří tvrdí, že goto má své místo. A skutečně má - v C.)

  • Zavést místní proměnnou a manipulovat s tím tokem řízení.
    Nevýhodou je, že kontrolní tok manipulovaný pomocí syntaxe (myslím break, return, if, while) je mnohem snazší sledovat než řídicí tok ovládaný stavem proměnných (protože tyto proměnné nemají žádný stav, když se podíváte na algoritmus).

Ve shromáždění je to ještě podivnější, protože při volání této funkce můžete přeskočit na libovolnou adresu ve funkci, což znamená, že máte téměř neomezený počet vstupních bodů do jakékoli funkce. (Někdy je to užitečné. Takové thunky jsou běžnou technikou pro kompilátory k implementaci úpravy ukazatele this ukazatele potřebné pro volání funkcí virtual ve scénářích více dědičnosti v C++.)

Pokud musíte zdroje spravovat ručně, využití možností zadávání nebo ukončování funkce kdekoli vede ke složitějšímu kódu, a tedy k chybám. Proto se objevila škola myšlení, která propagovala SESE, aby získala čistší kód a méně chyb.


Pokud však některý jazyk obsahuje výjimky, (téměř) jakákoli funkce může být předčasně ukončena (téměř) v jakémkoli okamžiku, takže je třeba přesto učinit opatření pro předčasný návrat. (Myslím, že finally se k tomu používá hlavně v Java a using (při implementaci IDisposable, finally jinak)) v C #; C++ místo toho zaměstnává RAII .) Jakmile to uděláte, nemůžete se vám nepodaří vyčistit po sobě kvůli do raného return prohlášení, takže co je pravděpodobně nejsilnější argument ve prospěch SESE, zmizelo.

To ponechává čitelnost. Samozřejmě, že funkce 200 LoC s půl tuctem příkazů return se náhodně posypala, není to dobrý programovací styl a nedělá se pro čitelný kód. Takovou funkci by však nebylo snadné pochopit i bez těchto předčasných návratů.

V jazycích, kde zdroje nejsou nebo by neměly být spravovány ručně, je při dodržování staré úmluvy SESE jen malá nebo žádná hodnota. OTOH, jak jsem argumentoval výše, SESE často kód komplikuje. Je to dinosaurus, který (s výjimkou C) nezapadá do většiny dnešních jazyků dobře. Místo toho, aby pomáhal srozumitelnosti kódu, brání mu.


Proč Java programátoři se drží tohoto? Nevím, ale od mého (venku) POV, Java vzal mnoho konvencí od C (kde dávají smysl) a aplikují je na svůj OO= svět (kde jsou k ničemu nebo úplně špatný), kde se jim nyní drží, bez ohledu na to, jaké náklady. (Jako konvenci definovat všechny vaše proměnné na začátku oboru.)

Programátoři se drželi všech druhů podivných zápisů z iracionálních důvodů. (Hluboko vnořené strukturální příkazy - „šipky“ - byly v jazycích jako Pascal, kdysi považovány za krásný kód.) Zdá se, že použití čistě logického odůvodnění k tomu nepřesvědčí většinu z nich, aby se odchýlila od svých zavedených způsobů. Nejlepší způsob, jak změnit tyto návyky, je pravděpodobně naučit je brzy dělat to, co je nejlepší, nikoli co je obvyklé. Vy jako učitel programování to máte ve své ruce. :)

921
sbi

Na jedné straně, jednoduché návratové příkazy usnadňují protokolování, jakož i formy ladění, které se spoléhají na protokolování. Mnohokrát si pamatuji, že jsem musel funkci omezit na jediný návrat, abych vytiskl návratovou hodnotu v jednom bodě.

  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;
  }

Na druhou stranu to můžete změnit na function(), který volá _function() a zaznamená výsledek.

82
perreal

"Single Entry, Single Exit" vznikl revolucí strukturovaného programování na začátku 70. let, kterou odstartoval dopis Edsgera W. Dijkstra editoru " GOTO prohlášení považováno za škodlivé ". Koncepce strukturovaného programování byly podrobně popsány v klasické knize „Strukturované programování“ od Ole Johan-Dahl, Edsger W. Dijkstra a Charles Anthony Richard Hoare.

„GOTO prohlášení považováno za škodlivé“ je nutné si přečíst i dnes. Strukturované programování je datováno, ale stále velmi, velmi obohacující, a mělo by být na vrcholu seznamu „Musí číst“ každého vývojáře, daleko nad čímkoli od např. Steve McConnell. (Dahlova sekce popisuje základní třídy v Simula 67, které jsou technickým základem pro třídy v C++ a všechny objektově orientované programování.)

53
John R. Strohm

Propojit Fowlera je vždy snadné.

Jedním z hlavních příkladů, které jdou proti SESE, jsou ochranné doložky:

Nahrazení vnořených podmíněných ustanoveními o ochranných doložkách

Pro všechny zvláštní případy používejte ochranné doložky

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();
};  

Více informací viz strana 250 z Refaktoring ...

36
Pieter B

Napsal jsem na toto téma blogový příspěvek zpět.

Sečteno podtrženo, že toto pravidlo pochází z věku jazyků, které nemají sběr odpadu ani zpracování výjimek. Neexistuje žádná formální studie, která by ukázala, že toto pravidlo vede k lepšímu kódu v moderních jazycích. Neváhejte jej ignorovat, pokud to povede ke kratšímu nebo čitelnějšímu kódu. Chlapi Java trvající na tom, že to trvají) jsou slepě a nezpochybňováni po zastaralém, zbytečném pravidlu.

Tato otázka byla také položena na Stackoverflow

11
Anthony

Jeden návrat usnadňuje refactoring. Zkuste provést „extrakční metodu“ do vnitřního těla smyčky for, která obsahuje návrat, zlomení nebo pokračování. To se nezdaří, protože jste přerušili tok řízení.

Jde o to: Myslím, že nikdo předstírá, že píše perfektní kód. Kód je proto pravidelně pod refaktoringem „vylepšován“ a rozšiřován. Mým cílem by tedy bylo udržet můj kód co možná nejpříjemnější.

Často čelím problému, že musím úplně přeformulovat funkce, pokud obsahují jističe řízení toku a pokud chci přidat jen malou funkčnost. Toto je velmi náchylné k chybám, protože změníte celý tok řízení místo toho, abyste zavedli nové cesty k izolovaným vnořením. Pokud máte na konci pouze jeden návrat nebo pokud používáte stráž k opuštění smyčky, máte samozřejmě více vnoření a více kódu. Získáte však kompilátor a IDE podporované možnosti refaktoringu).

6
oopexpert

Zvažte skutečnost, že více příkazů pro vrácení je rovnocenné s tím, že máte příkazy GOTO k jednomu příkazu pro vrácení. Totéž platí pro příkazy break. Někteří je tedy jako já považují za GOTO pro všechny záměry a účely.

Nepovažuji však tyto typy GOTO za škodlivé a neváhám použít skutečnou GOTO v mém kódu, pokud pro to zjistím dobrý důvod.

Moje obecné pravidlo je, že GOTO jsou pouze pro řízení toku. Nikdy by neměly být použity pro žádné opakování a nikdy byste neměli GOTO „nahoru“ nebo „vzad“. (což je, jak fungují přestávky/výnosy)

Jak již uvedli ostatní, je třeba si přečíst prohlášení GOTO považováno za škodlivé
Mějte však na paměti, že to bylo napsáno v roce 1970, kdy byly GOTO velmi nadužívány. Ne každá GOTO je škodlivá a já bych jejich použití neodradil, pokud je nepoužíváte místo běžných konstruktů, ale spíše v podivném případě, že by použití běžných konstruktů bylo velmi nepohodlné.

Zjistil jsem, že jejich použití v chybových případech, kdy je třeba uniknout oblasti z důvodu selhání, ke kterému by nikdy nemělo dojít v běžných případech, které jsou občas užitečné. Ale měli byste také zvážit uvedení tohoto kódu do samostatné funkce, abyste se mohli vrátit brzy, místo použití GOTO ... ale někdy je to také nevhodné.

5
user606723

Cyklomatická složitost

Viděl jsem SonarCube používat vícenásobné prohlášení o návratu pro určení cyklomatické složitosti. Čím více prohlášení o návratu, tím vyšší je cyklomatická složitost

Vrátit změnu typu

Více návratů znamená, že se musíme změnit na více místech ve funkci, když se rozhodneme změnit náš typ návratu.

Více opuštění

Je obtížnější ladit, protože logiku je třeba pečlivě prostudovat ve spojení s podmíněnými příkazy, abychom pochopili, co způsobilo vrácenou hodnotu.

Refactored Solution

Řešením více návratových příkazů je jejich nahrazení polymorfismem, který má jediný návrat po vyřešení požadovaného implementačního objektu.

4
Sorter