Jedno return, wiele return?

0

Ostatnio na zajęciach z podstaw programowania dowiedzialem się, że moi wykładowcy/ćwiczeniowcy strasznie są cięci na ilość returnów w metodzie.
Otóż wg nich jest tak, że metoda musi miec tylko JEDEN i nie wiecej returnow. Tzn jak mamy jakis prosty algorytm, ktory iteruje po tablicy i czegos szuka i jak znajdzie to zwraca to nie piszemy return w środku pętli, tylko tworzymy zmienną przed tą pętlą w każdym miejscu gdzie mielismy return przypisujemy wartość do tej zmiennej, a na samym koncu metody ja zawracamy. Dla rozjaśnienia:

niby źle:

int funkcja(){
    for(..){
        if(cos) return cosTam;
    }
    return cosInnegoNPDefaultowego;
}

niby dobrze:

int funkcja(){
    int zwrot;
    for(..){
        if(cos) zwrot = cosTam;
    }
    zwrot = cosInnegoNPDefaultowego;

    return zwrot
}

I tak sie zastanawiam na ile to jest ważne? mnie osobiscie lepiej czyta się kod gdzie return jest w pętli. Może nie dla wszystkich przypadków, ale dla takich exampli jak wyżej: po co ta zmienna? Zadanie gdzie mielismy znalezc cos w tablicy, jesli wystepuje zwrocic indeks w tablicy, jesli nie istnieje -1 -> kroilismy na tworzenie zmiennej do zwracania itd. Troche śmierdzi sztuką dla sztuki

co o tym sądzicie? może to ja taki nieuk i ignorant :(

0

Przykład który pokazałeś jest bez sensu bo wymagałby jeszcze break w pętli. Ale dla sytuacji:

def fun():
    x = default
    if condition:
        x = y
    return x

Może to mieć pewien sens. Wynika to z faktu, że ta wersja pozwala lepiej zoptymalizować wywołanie funkcji, bo return, niezależnie od przejscia przez funkcje, zwróci wartość z tego samego miejsca w pamięci. W przypadku z dwoma returnami nie da się określić miejsca z którego zwrócimy wynik funkcji przed wykonaniem kodu do końca.

W efekcie CPU może na przykład dekodować sobie już kolejne rozkazy bo wiadomo co dalej sie będzie w kodzie wykonywać.

1

Widywałem oba rozwiązania i zależało to od umowy programistów,
którzy tworzyli projekt i ustalili wspólny sposób kodowania.

Częściej jednak spotykałem return na początku funkcji
w if, które sprawdzało czy podano poprawny argument
a jeśli nie to wychodziło od razu z funkcji.

Coś w poniższym stylu

int silnia(int n){

    // argument musi byc dodatni
    if(n < 0) 
       return -1;

   // reszta kodu
}

Szczególnie takie coś jest przydatne w językach skryptowych
gdzie nie definuje się, jakiego typu ma być argument
i np. trzeba sprawdzać czy tam gdzie argument ma być liczbą
ktoś nie podał tekstu lub tablicy/listy.


Widuję też takie kody z break co chyba też nie każdy uznaje

int funkcja(){
    int zwrot = wartosc_domyslna;

    for(..){
        if(cos) {
            zwrot = cosTam;
            break;
        }
    }
 
    return zwrot
}

ps. Pascal za to nie ma komendy return
i ma taką budowę funkcji, że wyjście z niej
następuje tylko w jednym miejscu - na jej końcu.
Trzeba więc treść funkcji odpowiednio zbudować pod ten wymóg.

3

Jeden z moich kolegów w pracy był kiedyś wykładowcą i też jest zwolennikiem tej idei. Jak przyszedłem do pracy to nawet trochę googlałem, żeby zobaczyć o co biega. Generalnie moje wnioski są takie:

  1. Jest to praktyka z dawnych czasów, miała sens w C, już nie ma w C++ jeśli się odpowiednio pisze kod
  2. Argument zwolenników tej praktyki w mojej firmie jest taki, że jeśli się ma ileś returnów w długiej funkcji, to łatwo któryś przegapić. Tyle że dla mnie taka sytuacja wymaga skrócenia funkcji poprzez wydzielenie mniejszych funkcji, a nie wprowadzenia sztucznych ograniczeń.
  3. Strasznie mnie odpycha taki kod
bool stillOk = true;
...
if (stillOk)
{
    ...
}
if (stillOk)
{
    while (stillOk && someRealLogic())
    {
        ....
    }
}
if (stillOk)
...

Oczywiście należy zdawać sobie sprawę, że jeśli się ma brzydki i nieuporządkowany kod, to returny porozrzucane po różnych miejscach dodatkowo go komplikują i wprowadzają potencjalne błędy.

Jeden z linków do poczytania: http://programmers.stackexchange.com/questions/118703/where-did-the-notion-of-one-return-only-come-from

2

szybkie wyjście z funkcji przez returna jest świetnym pomysłem. Dokładnie w przypadku opisanym wcześniej, gdy sprawdzamy jakiś warunek. Nawet jeśli metoda nic nie zwraca

 public void DoSomeMagic(MyMagicClass myClass)
{
    if(myClass.MagicPower < minimalMagicPower)
   return;

   //
   // magic code here
   //
}

Jest czytelniejsze niż

 public void DoSomeMagic(MyMagicClass myClass)
{
    if(myClass.MagicPower >= minimalMagicPower)
    {
       //
       // magic code here
       //
   }
}

Gdy metoda rzeczywiście coś zwraca, ma to jeszcze większy sens i podnosi czytelność. Do tego takich warunków określających szybkie wyjście nie musi być tylko jeden.

0

A w Scali rzadko w ogóle używa się słówka return. W Scali wyrażenie na końcu bloku kodu nadaje wartość temu blokowi kodu. If też ma wartość, ale pod warunkiem, że ma odpowiadającego else'a. Stąd można tak ustrukturyzować funkcję, żeby nie trzeba było w ogóle pisać słówka return.

Przykład:

object Main {
  def computeRoots(a: Double, b: Double, c: Double): List[Double] = {
    val delta = b * b - 4 * a * c
    if (delta < 0) {
      Nil
    } else {
      val center = -b / (2 * a)
      if (delta == 0) {
        List(center)
      } else {
        val dist = Math.sqrt(delta) / (2 * a)
        List(center - dist, center + dist)
      }
    }
  }

  val format = (p: (Double, Double, Double), roots: List[Double]) => {
    val (a, b, c) = p
    s"a = $a, b = $b, c = $c, roots = $roots"
  }

  def main(args: Array[String]): Unit = {
    val testCases = List(
      (1d, 8d, 3d),
      (4d, 7d, 3d),
      (6d, 2d, 3d),
      (1d, 4d, 3d),
      (2d, 4d, 2d))
    testCases.zip(testCases.map((computeRoots _).tupled)).map(format.tupled)
      .foreach(println)
  }
}

Moim zdaniem bardzo dobre rozwiązanie :]

7

Reguła może i słuszna, ale lepszą zasadą jest nie pisanie długich funkcji.
Jak funkcja/metoda będzie prosta, to prawie na pewno będzie miała jedno return, a nawet jeśli trafią się dwa return to nie zaszkodzi to czytelności kodu.
Jak ktoś pisze funkcje na sto lub więcej linii z dużą ilością ifów albo pętli, to faktycznie return może napsuć krwi.

Dlatego zalecałby trzymania się zasady prostych funkcji, a zasada jednego return stanie się nieistotna lub trywialna do egzekwowania.

1

Ignorancją jest myśleć że to takie widzi-misie.

To jest zasada, której też się trzymam, z pogranicza trochę defensive programming i "how to write maintable code" ("un" celowo usunięte).

Po pierwsze: nie jest to uniwersalna zasada.

Nie ma zastosowania w ASM (tam wyskoki ASAP - z podprocedur - są jak najbardziej OK i co zwracasz widać jak na dłoni).
Możliwe że jest bez sensu w językach funkcyjnych (na razie w takich nie pisałem poza może LISP-em).

Gdzie ma zastosowanie?
W C++11 w funkcjach constexpr: http://en.cppreference.com/w/cpp/language/constexpr
W funkcjach które wiemy że będą zmieniane.
W językach które nie wspierają postconditions tak łatwo jak Ada czy Eiffel.
Oraz w procedurach tam gdzie są już skutki uboczne wywołania (zmiana stanu) i skutki te mamy zamiar rozszerzać.

Wyobrażanie sobie że jedno return prowadzi do czegoś negatywnego jest jak obrażanie się że w if chcemy wpisać więcej niż jedną instrukcję do wykonania i dlatego wymagamy zawsze klamerek (w językach C-podobnych).

Edit: takich dyskusji w necie jest pewnie multum, tutaj jeden przykład: http://stackoverflow.com/questions/36707/should-a-function-have-only-one-return-statement

0
furas napisał(a)

ps. Pascal nie ma komendy return i ma taką budowę funkcji, że wyjście z niej następuje tylko w jednym miejscu - na jej końcu.

Nie ma słowa kluczowego return, ale:

  • z funkcji wychodzi komenda exit, i można jej użyć wielokrotnie w ciele jednej funkcji,
  • wartość zwracaną przypisujemy po prostu do nazwy funkcji tak jakby była zmienną, foobar := 42; albo (w Delphi) Result := 42;. można wielokrotnie przypisywać wartość, w przeciwieństwie do returna z C, który od razu wychodzi.

A co do samej zasady „jednego returna”: uważam że to przesadyzm by robić z tego ścisłą zasadę. Jak najbardziej wczesny return jest wskazany przy sprawdzaniu poprawności parametrów. Również wewnątrz algorytmu może być przydatny, a jego unikanie będzie tylko gmatwać kod i mnożyć breaki i ify (to może break albo if też ma nie być?).
3

To i ja dorzucę swoje trzy grosze do dyskusji. Historycznie ma ona uwarunkowanie w tym jak były budowane procesory. Return w połowie pętli oznacza, że trzeba wykonać dużo dodatkowej pracy związanej z wycofaniem danych. Niektóre procesory słabo sobie z tym radzą. Znowuż inne radziły sobie z tym lepiej i dla nich przewidziano "optymalizacje" w kodzie w rodzaju "if then return", czyli sprawdzenia stanu i natychmiastowego wyjścia z metody. Dziś ma to trochę mniejsze znaczenie, choć np. karty graficzne nadal nie radzą sobie ze skokami i jak mają tego dużo na pokładzie to zaczynają mulić :)

Istnieje coś takiego jak złożoność cyklomatyczna. Jest to jedna z tych dziwnych miar jakości kodu, o których wiemy, ale nie zwracamy na nie uwagi.

Funkcja/metoda, która ma wiele punktów wyjścia, czyli instrukcji return albo jawnego wyrzucania wyjątków, jest bardziej złożona. Utrudnia to analizę (statyczną) kodu, zarówno manualną jak i automatyczną. To powoduje, że kod trudniej jest przetestować. Musisz analizować w testach wszystkie warunki brzegowe dla wszystkich wyjść. W dodatku wiele instrukcji return może, ale nie musi, świadczyć o tym, że metoda ma za dużą odpowiedzialność oraz dużą liczbę zagłębień.
W końcu istnieje cała grupa języków gdzie nie ma jawnego return tak jak w przykłądzie @Wibowita w Scali. W takim przypadku pisanie kodu, który ma jeden punkt wyjścia jest ogromnym ułatwieniem dla osoby, która będzie go czytać.

http://koziolekweb.pl/2011/10/22/ekstremalna-obiektowosc-w-praktyce-%E2%80%93-czesc-1-tylko-jeden-poziom-zaglebienia-na-metode/
http://koziolekweb.pl/2011/10/26/ekstremalna-obiektowosc-w-praktyce-%E2%80%93-czesc-2-nie-uzywaj-slowa-kluczowego-else/

1 użytkowników online, w tym zalogowanych: 0, gości: 1