Dlaczego funkcja miałaby mieć tylko jeden punkt wyjścia? [Zamknięte]

97

Zawsze słyszałem o funkcji pojedynczego punktu wyjścia jako o złym sposobie kodowania, ponieważ tracisz czytelność i wydajność. Nigdy nie słyszałem, żeby ktoś argumentował po drugiej stronie.

Myślałem, że ma to coś wspólnego z CS, ale to pytanie zostało odrzucone przy wymianie stosów teorii.

hex bob-omb
źródło
6
Odpowiedź jest taka, że ​​nie ma odpowiedzi, która jest zawsze poprawna. Często łatwiej jest kodować z wieloma wyjściami. Odkryłem również (podczas aktualizacji kodu powyżej), że modyfikowanie / rozszerzanie kodu było trudniejsze z powodu tych samych wielokrotnych wyjść. Nasza praca polega na podejmowaniu indywidualnych decyzji w każdym przypadku. Kiedy decyzja zawsze ma „najlepszą” odpowiedź, nie ma takiej potrzeby.
JS.
1
@finnw faszystowscy modyfikatorzy usunęli ostatnie dwa pytania, aby upewnić się, że będą musieli odpowiedzieć ponownie, i znowu, i jeszcze raz
Maarten Bodewes,
Pomimo słowa „kłócić się” w pytaniu, naprawdę nie sądzę, aby było to pytanie oparte na opiniach. Jest to dość istotne dla dobrego projektu itp. Nie widzę powodu, aby go zamknąć, ale w / e.
Ungeheuer
1
Pojedynczy punkt wyjścia upraszcza debugowanie, odczytywanie, pomiary i dostrajanie wydajności, refaktoryzację. Jest to obiektywne i istotne. Korzystanie z wczesnych zwrotów (po sprawdzeniu prostych argumentów) zapewnia rozsądne połączenie obu stylów. Biorąc pod uwagę zalety pojedynczego punktu wyjścia, zaśmiecanie kodu wartościami zwracanymi jest po prostu dowodem na leniwego, niechlujnego, nieostrożnego programistę - i przynajmniej prawdopodobnie nie lubienie szczeniąt.
Rick O'Shea,

Odpowiedzi:

108

Istnieją różne szkoły myślenia iw dużej mierze sprowadza się to do osobistych preferencji.

Jednym z nich jest to, że mniej zagmatwane jest, jeśli istnieje tylko jeden punkt wyjścia - masz jedną ścieżkę przez metodę i wiesz, gdzie szukać wyjścia. Z drugiej strony, jeśli używasz wcięć do reprezentowania zagnieżdżenia, twój kod zostanie masowo wcięty w prawo i bardzo trudno będzie śledzić wszystkie zagnieżdżone zakresy.

Innym jest to, że możesz sprawdzić warunki wstępne i wyjść wcześnie na początku metody, dzięki czemu wiesz w treści metody, że pewne warunki są prawdziwe, bez wcięcia całej metody o 5 mil w prawo. Zwykle minimalizuje to liczbę zakresów, o które musisz się martwić, co znacznie ułatwia śledzenie kodu.

Trzecią jest to, że możesz wyjść gdziekolwiek zechcesz. Kiedyś było to bardziej zagmatwane, ale teraz, gdy mamy edytory i kompilatory do kolorowania składni, które wykrywają nieosiągalny kod, jest o wiele łatwiejsze do zrobienia.

Jestem w środku obozu. Egzekwowanie pojedynczego punktu wyjścia jest bezcelowym lub wręcz przynoszącym efekt przeciwny do zamierzonego ograniczeniem IMHO, podczas gdy losowe wyjście z całej metody może czasami prowadzić do bałaganu i trudnej do przestrzegania logiki, w której trudno jest zobaczyć, czy dany fragment kodu będzie, czy nie będzie wykonany. Jednak „bramkowanie” metody umożliwia znaczne uproszczenie całości metody.

Jason Williams
źródło
1
W singe exitparadygmacie głębokiego zagnieżdżenia można uniknąć za pomocą go tostwierdzeń. Dodatkowo dostaje się możliwość wykonania postprocessingu pod lokalną Erroretykietą funkcji, co jest niemożliwe w przypadku wielu returns.
Ant_222,
2
Zwykle istnieje dobre rozwiązanie, które pozwala uniknąć konieczności wizyty. O wiele bardziej wolę „return (Fail (...))” i wstawić udostępniony kod czyszczenia do metody Fail. Może to wymagać przekazania kilku lokalizacji lokalnych, aby zwolnić pamięć itp., Ale jeśli nie jesteś w kodzie krytycznym dla wydajności, jest to zwykle znacznie czystsze rozwiązanie niż goto IMO. Pozwala również na udostępnianie podobnego kodu czyszczenia przez kilka metod.
Jason Williams
Istnieje optymalne podejście oparte na obiektywnych kryteriach, ale możemy zgodzić się, że istnieją szkoły myślenia (poprawne i niepoprawne) i sprowadza się to do osobistych preferencji (preferencji za lub przeciw poprawnemu podejściu).
Rick O'Shea
39

Moje ogólne zalecenie jest takie, aby instrukcje return w praktyce znajdowały się albo przed pierwszym kodem, który ma jakiekolwiek skutki uboczne, albo po ostatnim kodzie, który ma jakiekolwiek skutki uboczne. Rozważałbym coś takiego:

  if (! argument) // Sprawdź, czy nie jest null
    return ERR_NULL_ARGUMENT;
  ... przetwarza argument niezerowy
  jeśli (ok)
    return 0;
  jeszcze
    powrót ERR_NOT_OK;

jaśniejsze niż:

  int return_value;
  if (argument) // Wartość różna od null
  {
    .. przetwarzany argument niezerowy
    .. odpowiednio ustawić wynik
  }
  jeszcze
    wynik = ERR_NULL_ARGUMENT;
  wynik zwrotu;

Jeśli jakiś warunek powinien uniemożliwić funkcji wykonanie czegokolwiek, wolę wcześniej wrócić z funkcji w miejscu powyżej punktu, w którym funkcja zrobiłaby cokolwiek. Jednak gdy funkcja podejmie działania ze skutkami ubocznymi, wolę powrócić od dołu, aby wyjaśnić, że wszystkie skutki uboczne muszą zostać rozwiązane.

supercat
źródło
Twój pierwszy przykład, dotyczący zarządzania okzmienną, wygląda dla mnie jako podejście z pojedynczym zwrotem. Ponadto blok if-else można po prostu przepisać na:return ok ? 0 : ERR_NOT_OK;
Melebius,
2
Pierwszy przykład ma returnna początku znak przed całym kodem, który robi wszystko. Jeśli chodzi o używanie ?:operatora, zapisanie go w oddzielnych wierszach ułatwia w wielu IDE dołączenie punktu przerwania debugowania do scenariusza, w którym nie jest dobrze. Przy okazji, prawdziwym kluczem do „pojedynczego punktu wyjścia” jest zrozumienie, że liczy się to, że dla każdego konkretnego wywołania normalnej funkcji punktem wyjścia jest punkt bezpośrednio po wywołaniu . Obecnie programiści uważają to za coś oczywistego, ale nie zawsze tak było. W niektórych rzadkich przypadkach kod może wymagać
obejścia
... które wychodzą przez warunkowe lub obliczone gotos. Generalnie wszystko, co ma wystarczającą ilość zasobów do zaprogramowania w czymkolwiek innym niż język asemblera, będzie w stanie obsłużyć stos, ale napisałem kod asemblera, który musiał działać z bardzo ścisłymi ograniczeniami (aż do ZERO bajtów pamięci RAM w jednym przypadku), w takich przypadkach pomocne może być posiadanie wielu punktów wyjścia.
supercat
1
Tak zwany jaśniejszy przykład jest znacznie mniej jasny i trudny do odczytania. Jeden punkt wyjścia jest zawsze łatwiejszy do odczytania, łatwiejszy w utrzymaniu, łatwiejszy do debugowania.
GuidoG
8
@GuidoG: Każdy wzorzec może być bardziej czytelny, w zależności od tego, co pojawia się w pominiętych sekcjach. Używanie "return x;" wyjaśnia, że ​​jeśli instrukcja zostanie osiągnięta, zwracaną wartością będzie x. Korzystanie z „result = x;” pozostawia otwartą możliwość, że coś innego może zmienić wynik, zanim zostanie on zwrócony. Może to być przydatne, gdyby faktycznie zmiana wyniku była konieczna, ale programiści badający kod musieliby go sprawdzić, aby zobaczyć, jak wynik może się zmienić, nawet jeśli odpowiedź brzmi „nie może”.
supercat
15

Pojedynczy punkt wejścia i wyjścia był oryginalną koncepcją programowania strukturalnego w porównaniu z kodowaniem krok po kroku Spaghetti. Istnieje przekonanie, że wiele funkcji punktów wyjścia wymaga więcej kodu, ponieważ trzeba odpowiednio wyczyścić przestrzeń pamięci przydzieloną dla zmiennych. Rozważmy scenariusz, w którym funkcja przydziela zmienne (zasoby), a wczesne opuszczenie funkcji i bez odpowiedniego czyszczenia spowoduje wycieki zasobów. Ponadto skonstruowanie czyszczenia przed każdym wyjściem spowodowałoby utworzenie dużej ilości nadmiarowego kodu.

PSS
źródło
To naprawdę nie jest problem z RAII
BigSandwich,
14

W większości przypadków sprowadza się to do potrzeb produktu. W „dawnych czasach” kod spaghetti z wieloma punktami powrotu powodował wycieki pamięci, ponieważ programiści preferujący tę metodę zazwyczaj nie usuwali dobrze. Pojawiły się również problemy z „utratą” przez niektóre kompilatory odniesienia do zmiennej zwracanej, gdy stos był zdejmowany podczas powrotu, w przypadku powrotu z zagnieżdżonego zakresu. Bardziej ogólny problem dotyczył kodu ponownego wejścia, który próbuje uzyskać stan wywołania funkcji dokładnie taki sam, jak stan jej powrotu. Mutatory oop naruszyły to i koncepcja została odłożona na półkę.

Istnieją produkty, w szczególności jądra, które wymagają szybkości zapewnianej przez wiele punktów wyjścia. Te środowiska mają zwykle własną pamięć i zarządzanie procesami, więc ryzyko wycieku jest zminimalizowane.

Osobiście lubię mieć pojedynczy punkt wyjścia, ponieważ często używam go do wstawiania punktu przerwania w instrukcji return i wykonywania kodu sprawdzającego, w jaki sposób kod określił to rozwiązanie. Mógłbym po prostu podejść do wejścia i przejść przez nie, co robię w przypadku rozwiązań szeroko zagnieżdżonych i rekurencyjnych. Jako recenzent kodu, wielokrotne zwroty w funkcji wymagają znacznie głębszej analizy - więc jeśli robisz to, aby przyspieszyć implementację, okradasz Petera, aby uratować Paula. Przegląd kodu będzie wymagał więcej czasu, unieważniając domniemanie skutecznej implementacji.

- 2 centy

Więcej informacji można znaleźć w tym dokumencie: NISTIR 5459

sscheider
źródło
8
multiple returns in a function requires a much deeper analysistylko jeśli funkcja jest już ogromna (> 1 ekran), w przeciwnym razie ułatwia analizę
dss539
2
wielokrotne zwroty nigdy nie ułatwiają analizy, po prostu
odwrotnie
1
łącze nie działa (404).
fusi
1
@fusi - znalazłem go na archive.org i zaktualizowałem link tutaj
sscheider
4

Moim zdaniem rada wyjścia z funkcji (lub innej struktury kontrolnej) tylko w jednym miejscu jest często wyprzedana. Zwykle podaje się dwa powody, aby wyjść tylko w jednym punkcie:

  1. Kod pojedynczego wyjścia jest podobno łatwiejszy do odczytania i debugowania. (Przyznaję, że nie myślę zbytnio o tym powodzie, ale jest on podany. Znacznie łatwiejsze do odczytania i debugowania jest kod jednokrotnego wejścia .)
  2. Kod pojedynczego wyjścia łączy i zwraca bardziej przejrzysty sposób.

Drugi powód jest subtelny i ma pewne zalety, zwłaszcza jeśli funkcja zwraca dużą strukturę danych. Jednak nie martwiłbym się tym zbytnio, poza ...

Jeśli jesteś studentem, chcesz zdobyć najwyższe oceny w swojej klasie. Rób to, co preferuje instruktor. Ze swojej perspektywy prawdopodobnie ma dobry powód; więc przynajmniej poznasz jego perspektywę. Ma to wartość samą w sobie.

Powodzenia.

thb
źródło
4

Kiedyś byłem zwolennikiem stylu pojedynczego wyjścia. Moje rozumowanie pochodziło głównie z bólu ...

Pojedyncze wyjście jest łatwiejsze do debugowania.

Biorąc pod uwagę techniki i narzędzia, które mamy dzisiaj, jest to znacznie mniej rozsądne stanowisko, ponieważ testy jednostkowe i rejestrowanie mogą sprawić, że pojedyncze wyjście stanie się niepotrzebne. To powiedziawszy, kiedy trzeba obserwować wykonanie kodu w debugerze, znacznie trudniej było zrozumieć kod zawierający wiele punktów wyjścia i pracować z nim.

Stało się to szczególnie prawdziwe, gdy trzeba było wtrącać przypisania w celu zbadania stanu (zastąpione wyrażeniami obserwującymi w nowoczesnych debugerach). Zbyt łatwo było również zmienić przepływ sterowania w sposób, który ukrył problem lub całkowicie zepsuł wykonanie.

Metody z pojedynczym wyjściem były łatwiejsze do przejścia w debugerze i łatwiejsze do rozłączenia bez łamania logiki.

Michael Murphree
źródło
0

Odpowiedź jest bardzo zależna od kontekstu. Jeśli tworzysz GUI i masz funkcję, która inicjuje API i otwiera okna na początku twojego maina, będzie ona pełna wywołań, które mogą powodować błędy, z których każdy może spowodować zamknięcie instancji programu. Jeśli użyjesz zagnieżdżonych instrukcji JEŻELI i wprowadzisz wcięcie, kod może szybko zostać bardzo przekrzywiony w prawo. Zwracanie błędów na każdym etapie może być lepsze i bardziej czytelne, a jednocześnie łatwe do debugowania z kilkoma flagami w kodzie.

Jeśli jednak testujesz różne warunki i zwracasz różne wartości w zależności od wyników uzyskanych w metodzie, znacznie lepszą praktyką może być posiadanie jednego punktu wyjścia. Kiedyś pracowałem nad skryptami do przetwarzania obrazu w MATLAB-ie, które mogły być bardzo duże. Wiele punktów wyjścia może bardzo utrudnić śledzenie kodu. Instrukcje przełączania były znacznie bardziej odpowiednie.

Najlepiej uczyć się w trakcie. Jeśli piszesz kod do czegoś, spróbuj znaleźć kod innych osób i zobaczyć, jak go implementują. Zdecyduj, które bity Ci się podobają, a które nie.

Flash_Steel
źródło
-6

Jeśli uważasz, że potrzebujesz wielu punktów wyjścia w funkcji, oznacza to, że funkcja jest za duża i robi za dużo.

Poleciłbym przeczytać rozdział o funkcjach w książce Roberta C. Martina Clean Code.

Zasadniczo powinieneś próbować pisać funkcje z maksymalnie 4 liniami kodu.

Kilka uwag z bloga Mike'a Longa :

  • Pierwsza zasada funkcji: powinny być małe
  • Druga zasada funkcji: powinny być mniejsze niż to
  • Bloki w instrukcjach if, while, for loop itp. Powinny mieć długość jednej linii
  • … A ten wiersz kodu będzie zwykle wywołaniem funkcji
  • Nie powinno być więcej niż jeden lub może dwa poziomy wcięcia
  • Funkcje powinny robić jedną rzecz
  • Wszystkie instrukcje funkcyjne powinny znajdować się na tym samym poziomie abstrakcji
  • Funkcja nie powinna mieć więcej niż 3 argumenty
  • Argumenty wyjściowe to zapach kodu
  • Przekazywanie flagi boolowskiej do funkcji jest naprawdę okropne. Z definicji wykonujesz dwie czynności w funkcji.
  • Skutki uboczne to kłamstwa.
Roger Dahl
źródło
29
4 linie? Co kodujesz, co zapewnia taką prostotę? Naprawdę wątpię, czy na przykład jądro Linuksa lub git to robi.
shinzou
7
„Przekazywanie flagi boolowskiej do funkcji jest naprawdę okropne. Z definicji wykonujesz w funkcji dwie rzeczy”. Zgodnie z definicją? Nie ... ta wartość logiczna może potencjalnie wpłynąć tylko na jedną z czterech linii. Chociaż zgadzam się z utrzymaniem małego rozmiaru funkcji, cztery jest trochę zbyt restrykcyjne. Należy to traktować jako bardzo luźną wytyczną.
Jesse,
12
Dodanie takich ograniczeń nieuchronnie spowoduje zagmatwanie kodu. Chodzi bardziej o to, aby metody były czyste i zwięzłe i trzymały się tylko tego, do czego są przeznaczone, bez niepotrzebnych skutków ubocznych.
Jesse
10
Jedna z nielicznych odpowiedzi na SO, którą chciałbym móc wielokrotnie przegłosować.
Steven Rands
8
Niestety ta odpowiedź robi wiele rzeczy i prawdopodobnie musiała zostać podzielona na wiele innych - wszystkie mniej niż cztery wiersze.
Eli