Czy istnieją dobre powody, dla których lepiej jest mieć tylko jedną instrukcję return w funkcji?
Czy jest w porządku, aby powrócić z funkcji, gdy jest to logicznie poprawne, co oznacza, że może być wiele instrukcji return w funkcji?
Czy istnieją dobre powody, dla których lepiej jest mieć tylko jedną instrukcję return w funkcji?
Czy jest w porządku, aby powrócić z funkcji, gdy jest to logicznie poprawne, co oznacza, że może być wiele instrukcji return w funkcji?
Odpowiedzi:
Często mam kilka stwierdzeń na początku metody powrotu do „łatwych” sytuacji. Na przykład:
public void DoStuff(Foo foo)
{
if (foo != null)
{
...
}
}
... można uczynić bardziej czytelnym (IMHO) w następujący sposób:
public void DoStuff(Foo foo)
{
if (foo == null) return;
...
}
Więc tak, myślę, że dobrze jest mieć wiele „punktów wyjścia” z funkcji / metody.
DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
Nikt nie wspomniał ani nie podał kodu Complete, więc zrobię to.
Zminimalizuj liczbę zwrotów w każdej procedurze . Trudniej jest zrozumieć rutynę, jeśli czytając ją na dole, nie jesteś świadomy możliwości, że powróciła gdzieś powyżej.
Użyj znaku powrotu, gdy poprawia to czytelność . W niektórych procedurach, gdy znasz odpowiedź, chcesz ją natychmiast przywrócić do procedury wywoływania. Jeśli procedura jest zdefiniowana w taki sposób, że nie wymaga żadnego czyszczenia, brak powrotu natychmiast oznacza, że musisz napisać więcej kodu.
Powiedziałbym, że niezwykle rozsądne byłoby arbitralne podejmowanie decyzji przeciwko wielu punktom wyjścia, ponieważ uważałem, że technika ta jest przydatna w praktyce w kółko , w rzeczywistości często przebudowałem istniejący kod do wielu punktów wyjścia dla zachowania przejrzystości. Możemy porównać dwa podejścia w ten sposób:
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;
}
Porównaj to z kodem, w którym dozwolonych jest wiele punktów wyjścia :
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;
}
Myślę, że ta ostatnia jest znacznie jaśniejsza. O ile mogę powiedzieć, krytyka wielu punktów wyjścia jest obecnie raczej archaicznym punktem widzenia.
Obecnie pracuję nad bazą kodów, w której dwie osoby pracujące nad nią ślepo popierają teorię „pojedynczego punktu wyjścia” i mogę powiedzieć, że z doświadczenia jest to okropna, okropna praktyka. To sprawia, że kod jest niezwykle trudny w utrzymaniu i pokażę ci dlaczego.
Z teorią „pojedynczego punktu wyjścia” nieuchronnie kończy się kod, który wygląda następująco:
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;
}
To nie tylko sprawia, że kod jest bardzo trudny do naśladowania, ale teraz powiedz później, że musisz wrócić i dodać operację między 1 a 2. Musisz wciąć tylko całą funkcję maniaków i powodzenia, upewniając się, że wszystkie twoje warunki / szelki if / else są odpowiednio dopasowane.
Ta metoda sprawia, że konserwacja kodu jest niezwykle trudna i podatna na błędy.
Programowanie strukturalne mówi, że powinieneś mieć tylko jedną instrukcję return na funkcję. Ma to na celu ograniczenie złożoności. Wiele osób, takich jak Martin Fowler, twierdzi, że łatwiej jest pisać funkcje z wieloma instrukcjami return. Przedstawia ten argument w klasycznej książce o refaktoryzacji, którą napisał. Działa to dobrze, jeśli zastosujesz się do jego innych rad i napiszesz małe funkcje. Zgadzam się z tym punktem widzenia i tylko ściśle ustrukturyzowani purystycy programowania przestrzegają pojedynczych instrukcji return na funkcję.
GOTO
do sterowania przepływem sterowania, nawet gdy istniała funkcja. Nigdy nie mówi „nigdy nie używaj GOTO
”.
Jak zauważa Kent Beck podczas omawiania klauzul ochronnych we Wzorcach implementacyjnych, w których rutyna ma jeden punkt wejścia i wyjścia ...
”miało zapobiec nieporozumieniom możliwym przy wskakiwaniu i wychodzeniu z wielu lokalizacji w tej samej procedurze. To miało sens, gdy zastosowano je do FORTRAN lub programów w języku asemblera napisanych z dużą ilością danych globalnych, w których nawet zrozumienie, które instrukcje zostały wykonane, było ciężką pracą. przy użyciu małych metod i głównie danych lokalnych jest to niepotrzebnie konserwatywne ”.
Uważam, że funkcja napisana za pomocą klauzul ochronnych jest znacznie łatwiejsza do naśladowania niż jedna długa zagnieżdżona wiązka if then else
instrukcji.
W funkcji, która nie ma skutków ubocznych, nie ma dobrego powodu, aby mieć więcej niż jeden zwrot i powinieneś napisać je w funkcjonalnym stylu. W metodzie z efektami ubocznymi rzeczy są bardziej sekwencyjne (indeksowane czasowo), więc piszesz w trybie rozkazującym, używając instrukcji return jako polecenia, aby zatrzymać wykonywanie.
Innymi słowy, jeśli to możliwe, faworyzuj ten styl
return a > 0 ?
positively(a):
negatively(a);
Nad tym
if (a > 0)
return positively(a);
else
return negatively(a);
Jeśli zauważysz, że piszesz kilka warstw warunków zagnieżdżonych, prawdopodobnie istnieje sposób, aby to zmienić, np. Używając listy predykatów. Jeśli okaże się, że twoje ifs i wady są daleko od siebie syntaktyczne, możesz podzielić to na mniejsze funkcje. Blok warunkowy obejmujący więcej niż ekran tekstu jest trudny do odczytania.
Nie ma twardej i szybkiej reguły, która miałaby zastosowanie do każdego języka. Coś takiego jak posiadanie pojedynczej instrukcji return nie poprawi twojego kodu. Ale dobry kod pozwala na pisanie funkcji w ten sposób.
Widziałem to w standardach kodowania dla C ++, które były kacem z C, ponieważ jeśli nie masz RAII lub innego automatycznego zarządzania pamięcią, musisz wyczyścić każdy zwrot, co oznacza albo wycinanie i wklejanie czyszczenia lub goto (logicznie to samo, co „wreszcie” w zarządzanych językach), które są uważane za złą formę. Jeśli używasz inteligentnych wskaźników i kolekcji w C ++ lub innym automatycznym systemie pamięci, nie ma ku temu silnego powodu, a wszystko sprowadza się do czytelności i bardziej do oceny.
auto_ptr
, możesz używać prostych wskaźników równolegle. Chociaż pisanie „zoptymalizowanego” kodu za pomocą nieoptymalizującego kompilatora byłoby dziwne.
try
... finally
w Javie) i musisz przeprowadzić konserwację zasobów, którą możesz zrobić za pomocą jednego zwraca na końcu metody. Zanim to zrobisz, powinieneś poważnie rozważyć zmianę kodu w celu pozbycia się sytuacji.
Opieram się na pomyśle, że instrukcje zwracane w środku funkcji są złe. Możesz użyć return, aby zbudować kilka klauzul ochronnych u góry funkcji i oczywiście powiedzieć kompilatorowi, co zwrócić na końcu funkcji bez problemu, ale zwroty w środku funkcji mogą być łatwe do pominięcia i mogą utrudniają interpretację funkcji.
Czy istnieją dobre powody, dla których lepiej jest mieć tylko jedną instrukcję return w funkcji?
Tak , są:
Pytanie to jest często przedstawiane jako fałszywa dychotomia między wieloma zwrotami lub głęboko zagnieżdżone, jeśli instrukcje. Prawie zawsze istnieje trzecie rozwiązanie, które jest bardzo liniowe (bez głębokiego zagnieżdżania) z tylko jednym punktem wyjścia.
Aktualizacja : Najwyraźniej wytyczne MISRA promują również pojedyncze wyjście .
Dla jasności nie twierdzę, że wielokrotne zwroty zawsze są złe. Ale biorąc pod uwagę równoważne rozwiązania, istnieje wiele dobrych powodów, aby preferować ten z jednym zwrotem.
Contract.Ensures
z wieloma punktami zwrotu.
goto
z wspólnego kodu czyszczenia, prawdopodobnie uprościłeś tę funkcję, aby return
na końcu kodu czyszczenia była jedna . Więc możesz powiedzieć, że rozwiązałeś problem goto
, ale powiedziałbym, że rozwiązałeś go przez uproszczenie do jednego return
.
Posiadanie pojedynczego punktu wyjścia zapewnia przewagę podczas debugowania, ponieważ pozwala ustawić pojedynczy punkt przerwania na końcu funkcji, aby zobaczyć, jaka wartość rzeczywiście zostanie zwrócona.
Ogólnie staram się mieć tylko jeden punkt wyjścia z funkcji. Są jednak chwile, że tak naprawdę kończy się to tworzeniem bardziej złożonego korpusu funkcji, niż jest to konieczne, w takim przypadku lepiej jest mieć wiele punktów wyjścia. To naprawdę musi być „wezwanie do oceny” oparte na wynikającej z tego złożoności, ale celem powinno być jak najmniej punktów wyjścia, bez poświęcania złożoności i zrozumiałości.
Nie, ponieważ nie żyjemy już w latach siedemdziesiątych . Jeśli twoja funkcja jest wystarczająco długa, aby wielokrotne zwroty stanowiły problem, jest za długa.
(Pomijając fakt, że każda funkcja wieloliniowa w języku z wyjątkami i tak będzie miała wiele punktów wyjścia).
Preferuję pojedyncze wyjście, chyba że naprawdę to komplikuje. Odkryłem, że w niektórych przypadkach wiele istniejących punktów może maskować inne, bardziej znaczące problemy projektowe:
public void DoStuff(Foo foo)
{
if (foo == null) return;
}
Po zobaczeniu tego kodu od razu zapytam:
W zależności od odpowiedzi na te pytania może to być to
W obu powyższych przypadkach kod można prawdopodobnie przerobić za pomocą asercji, aby upewnić się, że „foo” nigdy nie ma wartości zerowej, a odpowiednie elementy wywołujące uległy zmianie.
Istnieją dwa inne powody (specyficzne, jak sądzę, kod C ++), w których istnieje wiele, które mogą mieć negatywny wpływ. Są to rozmiar kodu i optymalizacje kompilatora.
Obiekt non-POD C ++ w zasięgu na wyjściu funkcji będzie miał wywołany destruktor. Jeśli istnieje kilka instrukcji return, może się zdarzyć, że w zasięgu znajdują się różne obiekty, więc lista wywoływanych destruktorów będzie inna. Dlatego kompilator musi wygenerować kod dla każdej instrukcji 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'
}
Jeśli problemem jest rozmiar kodu - warto go unikać.
Drugi problem dotyczy „Nazwanej optymalizacji wartości zwracanej” (inaczej Copy Elision, ISO C ++ '03 12.8 / 15). C ++ pozwala implementacji pominąć wywoływanie konstruktora kopiowania, jeśli może:
A foo () {
A a1;
// do something
return a1;
}
void bar () {
A a2 ( foo() );
}
Przyjmując kod w niezmienionej postaci, obiekt „a1” jest konstruowany w „foo”, a następnie jego konstrukcja kopiująca zostanie wywołana w celu utworzenia „a2”. Jednak eliminacja kopii pozwala kompilatorowi zbudować „a1” w tym samym miejscu na stosie co „a2”. Nie ma zatem potrzeby „kopiowania” obiektu po powrocie funkcji.
Wiele punktów wyjścia komplikuje pracę kompilatora w próbach wykrycia tego, a przynajmniej dla stosunkowo nowej wersji VC ++ optymalizacja nie miała miejsca, gdy ciało funkcji miało wiele zwrotów. Aby uzyskać więcej informacji, zobacz Optymalizacja nazwanych wartości zwrotnych w programie Visual C ++ 2005 .
throw new ArgumentNullException()
w C # w tym przypadku), naprawdę spodobały mi się twoje inne rozważania, wszystkie są dla mnie ważne i mogą być krytyczne w niektórych konteksty niszowe.
foo
jest testowany, nie ma nic wspólnego z tym tematem, czy chodzi o to, if (foo == NULL) return; dowork;
czyif (foo != NULL) { dowork; }
Posiadanie jednego punktu wyjścia zmniejsza złożoność cykliczną, a zatem teoretycznie zmniejsza prawdopodobieństwo wprowadzenia błędów w kodzie podczas jego zmiany. Praktyka sugeruje jednak, że potrzebne jest bardziej pragmatyczne podejście. Dlatego staram się mieć jeden punkt wyjścia, ale pozwalam, aby mój kod miał kilka, jeśli jest to bardziej czytelne.
Zmuszam się do użycia tylko jednej return
instrukcji, ponieważ w pewnym sensie wygeneruje zapach kodu. Pozwól mi wyjaśnić:
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;
}
(Warunki są arbritary ...)
Im więcej warunków, tym większa funkcja, tym trudniej jest ją odczytać. Jeśli więc zestroisz się z zapachem kodu, zdasz sobie z tego sprawę i zechcesz go zmodyfikować. Dwa możliwe rozwiązania to:
Wiele zwrotów
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;
}
Oddzielne funkcje
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);
}
To prawda, jest dłuższy i nieco niechlujny, ale w trakcie refaktoryzacji funkcji w ten sposób mamy
Powiedziałbym, że powinieneś mieć tyle, ile potrzeba, lub takie, które czynią kod czystszym (np. Klauzule ochronne ).
Osobiście nigdy nie słyszałem / nie widziałem żadnych „najlepszych praktyk”, które mówią, że powinieneś mieć tylko jedno oświadczenie zwrotne.
W większości przypadków wychodzę z funkcji tak szybko, jak to możliwe, na podstawie ścieżki logicznej (klauzule ochronne są tego doskonałym przykładem).
Uważam, że wielokrotne zwroty są zwykle dobre (w kodzie, który piszę w języku C #). Styl pojedynczego zwrotu jest pozostałością po C. Ale prawdopodobnie nie kodujesz w C.
Nie ma prawa wymagającego tylko jednego punktu wyjścia dla metody we wszystkich językach programowania . Niektórzy ludzie nalegają na wyższość tego stylu, a czasem podnoszą go do „reguły” lub „prawa”, ale przekonanie to nie jest poparte żadnymi dowodami ani badaniami.
Więcej niż jeden styl zwracania może być złym nawykiem w kodzie C, gdzie zasoby muszą być jawnie zwolnione, ale języki takie jak Java, C #, Python lub JavaScript, które mają konstrukcje takie jak automatyczne odśmiecanie i try..finally
bloki (i using
bloki w C # ), a ten argument nie ma zastosowania - w tych językach bardzo rzadko jest potrzebne scentralizowane ręczne zwalnianie zasobów.
Są przypadki, w których pojedynczy zwrot jest bardziej czytelny, a przypadki, w których nie jest. Sprawdź, czy zmniejsza liczbę wierszy kodu, czyści logikę lub zmniejsza liczbę nawiasów klamrowych i wcięć lub zmiennych tymczasowych.
Dlatego stosuj tyle zwrotów, ile odpowiada twojej wrażliwości artystycznej, ponieważ jest to kwestia układu i czytelności, a nie kwestia techniczna.
Mówiłem o tym bardziej szczegółowo na moim blogu .
Są dobre rzeczy do powiedzenia na temat posiadania jednego punktu wyjścia, podobnie jak złe rzeczy do powiedzenia na temat nieuchronnego programowania „strzałkowego”, które się pojawia .
Jeśli korzystam z wielu punktów wyjścia podczas sprawdzania poprawności danych wejściowych lub alokacji zasobów, staram się umieścić wszystkie „wyjścia błędów” bardzo wyraźnie na górze funkcji.
Zarówno artykuł o programowaniu spartańskim „SSDSLPedia”, jak i artykuł dotyczący punktu wyjścia z pojedynczą funkcją z „Wiki wzorcowego repozytorium portlandzkiego” mają wnikliwe argumenty na ten temat. Oczywiście jest jeszcze ten post do rozważenia.
Jeśli naprawdę chcesz jednego punktu wyjścia (w dowolnym języku nieobsługującym wyjątków), na przykład w celu uwolnienia zasobów w jednym miejscu, uważam, że staranne zastosowanie goto jest dobre; patrz na przykład ten wymyślony przykład (skompresowany w celu zapisania nieruchomości na ekranie):
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;
}
Osobiście nie lubię programowania strzał bardziej niż wielu punktów wyjścia, chociaż oba są przydatne, gdy są stosowane poprawnie. Oczywiście najlepiej jest ustrukturyzować program tak, aby nie wymagał żadnego z nich. Podział funkcji na wiele części zwykle pomaga :)
Chociaż robiąc to, okazuje się, że i tak mam wiele punktów wyjścia, jak w tym przykładzie, w którym jakaś większa funkcja została podzielona na kilka mniejszych funkcji:
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);
}
W zależności od projektu lub wytycznych kodowania większość kodu płyty kotła można zastąpić makrami. Na marginesie, podzielenie go w ten sposób sprawia, że funkcje g0, g1, g2 są bardzo łatwe do przetestowania indywidualnie.
Oczywiście w języku OO i języku obsługującym wyjątki nie używałbym takich instrukcji if (lub w ogóle, gdybym mógł to zrobić przy niewielkim wysiłku), a kod byłby o wiele prostszy. I niearrowy. I większość nieoficjalnych zwrotów prawdopodobnie byłaby wyjątkiem.
W skrócie;
Znasz powiedzenie - piękno jest w oczach patrzącego .
Niektórzy przysięgają przez NetBeans, inni przez IntelliJ IDEA , inni przez Python, a inni przez PHP .
W niektórych sklepach możesz stracić pracę, jeśli nalegasz na zrobienie tego:
public void hello()
{
if (....)
{
....
}
}
Pytanie dotyczy widoczności i łatwości konserwacji.
Jestem uzależniony od używania algebry boolowskiej w celu ograniczenia i uproszczenia logiki i korzystania z automatów stanów. Byli jednak byli koledzy, którzy uważali, że zastosowanie przeze mnie „technik matematycznych” w kodowaniu jest nieodpowiednie, ponieważ nie byłoby to widoczne i możliwe do utrzymania. I to byłaby zła praktyka. Przepraszam ludzi, stosowane przeze mnie techniki są dla mnie bardzo widoczne i łatwe do utrzymania - ponieważ gdy wrócę do kodu sześć miesięcy później, zrozumiem kod wyraźnie, widząc bałagan przysłowiowego spaghetti.
Hej kolego (jak mawiał były klient) rób, co chcesz, o ile wiesz, jak to naprawić, kiedy będę tego potrzebować.
Pamiętam 20 lat temu, że mój kolega został zwolniony za stosowanie strategii zwinnego rozwoju . Miał drobiazgowy plan przyrostowy. Ale jego menedżer krzyczał na niego: „Nie można stopniowo udostępniać funkcji użytkownikom! Musisz trzymać się wodospadu ”. Jego odpowiedzią dla kierownika było to, że stopniowy rozwój będzie bardziej precyzyjny w stosunku do potrzeb klienta. Wierzył w rozwój dla potrzeb klientów, ale kierownik wierzył w kodowanie zgodnie z „wymaganiami klienta”.
Często jesteśmy winni łamania normalizacji danych, granic MVP i MVC . Wstawiamy zamiast konstruować funkcję. Bierzemy skróty.
Osobiście uważam, że PHP to zła praktyka, ale co wiem. Wszystkie teoretyczne argumenty sprowadzają się do próby spełnienia jednego zestawu zasad
jakość = precyzja, łatwość konserwacji i rentowność.
Wszystkie pozostałe reguły znikają w tle. I oczywiście ta zasada nigdy nie zanika:
Lenistwo jest zaletą dobrego programisty.
Skłaniam się do korzystania z klauzul ochronnych, aby powrócić wcześniej i inaczej wyjść na końcu metody. Reguła pojedynczego wejścia i wyjścia ma znaczenie historyczne i była szczególnie pomocna w przypadku starszego kodu, który prowadził do 10 stron A4 dla jednej metody C ++ z wieloma zwrotami (i wieloma wadami). Niedawno przyjętą dobrą praktyką jest utrzymywanie niewielkich metod, co sprawia, że wielokrotne wyjścia są mniej przeszkodą dla zrozumienia. W poniższym przykładzie Kronoz skopiowanym z góry pytanie brzmi: co dzieje się w // Reszcie kodu ... ?:
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;
}
Zdaję sobie sprawę, że przykład jest nieco wymyślony, ale kusiłoby mnie, aby przekształcić pętlę foreach w instrukcję LINQ, którą można by następnie uznać za klauzulę ochronną. Ponownie, w na dobry przykład intencją kodu nie jest oczywiste i someFunction () może mieć inny efekt uboczny albo wynik może być stosowany w // reszty kodu ... .
if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;
Nadanie następującej funkcji refaktoryzowanej:
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;
}
null
zamiast rzucać wyjątek wskazujący, że argument nie został zaakceptowany?
Jednym z dobrych powodów, dla których mogę wymyślić konserwację kodu, jest jeden punkt wyjścia. Jeśli chcesz zmienić format wyniku, ... jest to o wiele prostsze do wdrożenia. Ponadto w celu debugowania możesz po prostu wstawić tam punkt przerwania :)
Powiedziawszy to, kiedyś musiałem pracować w bibliotece, w której standardy kodowania nakładały „jedną instrukcję zwrotną na funkcję”, i uważałem to za dość trudne. Piszę dużo kodu do obliczeń numerycznych i często zdarzają się „przypadki specjalne”, więc kod był trudny do naśladowania ...
Wiele punktów wyjścia jest wystarczających dla wystarczająco małych funkcji - to jest funkcji, którą można oglądać na jednym ekranie w całości. Jeśli długa funkcja zawiera również wiele punktów wyjścia, jest to znak, że funkcję można jeszcze bardziej podzielić.
To powiedziawszy, unikam funkcji wielokrotnego wyjścia, chyba że jest to absolutnie konieczne . Czułem ból błędów, które wynikają z pewnego błądzącego powrotu w jakiejś niejasnej linii w bardziej złożonych funkcjach.
Pracowałem ze strasznymi standardami kodowania, które wymusiły na tobie jedną ścieżkę wyjścia, a wynikiem jest prawie zawsze nieustrukturyzowane spaghetti, jeśli funkcja ta jest trywialna - kończysz się wieloma przerwami i dalej to przeszkadza.
if
instrukcji przed każdym wywołaniem metody, która zwraca sukces, czy nie :(
Pojedynczy punkt wyjścia - wszystkie inne rzeczy są równe - sprawia, że kod jest znacznie bardziej czytelny. Ale jest pewien haczyk: popularna konstrukcja
resulttype res;
if if if...
return res;
to podróbka, „res =” nie jest dużo lepsze niż „return”. Ma pojedynczą instrukcję powrotu, ale wiele punktów, w których funkcja faktycznie się kończy.
Jeśli masz funkcję z wieloma zwrotami (lub „res =” s), często dobrym pomysłem jest podzielenie jej na kilka mniejszych funkcji z jednym punktem wyjścia.
Moją zwykłą polityką jest posiadanie tylko jednej instrukcji return na końcu funkcji, chyba że złożoność kodu zostanie znacznie zmniejszona przez dodanie większej liczby. W rzeczywistości jestem raczej fanem Eiffla, który egzekwuje jedyną regułę zwrotu, ponieważ nie ma instrukcji return (jest tylko automatycznie utworzona zmienna „wynik”, w której można umieścić wynik).
Z pewnością istnieją przypadki, w których kod może być wyraźniejszy dzięki wielokrotnym zwrotom, niż w przypadku oczywistej wersji bez nich. Można argumentować, że potrzeba więcej przeróbek, jeśli masz funkcję, która jest zbyt złożona, aby była zrozumiała bez wielu instrukcji return, ale czasem dobrze jest być pragmatycznym w takich sprawach.
Jeśli uzyskasz więcej niż kilka zwrotów, może być coś nie tak z twoim kodem. W przeciwnym razie zgodziłbym się, że czasami miło jest móc powrócić z wielu miejsc w podprogramie, szczególnie gdy sprawia, że kod jest czystszy.
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 }
}
}
lepiej byłoby napisać w ten sposób
@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]
}
Uwaga: to był tylko szybki przykład
Głosuję za Pojedynczym powrotem na końcu jako wytyczną. Pomaga to w czyszczeniu wspólnego kodu ... Na przykład spójrz na następujący kod ...
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);
}
}
Jest to prawdopodobnie niezwykła perspektywa, ale myślę, że każdy, kto uważa, że należy faworyzować wiele instrukcji zwrotnych, nigdy nie musiał używać debugera na mikroprocesorze, który obsługuje tylko 4 punkty przerwania sprzętowego. ;-)
Podczas gdy problemy z „kodem strzałki” są całkowicie poprawne, jednym z problemów, który wydaje się znikać podczas używania wielu instrukcji return, jest sytuacja, w której używasz debuggera. Nie masz dogodnej pozycji catch-all, aby ustawić punkt przerwania, aby zagwarantować, że zobaczysz wyjście, a tym samym warunek powrotu.
Im więcej instrukcji return masz w funkcji, tym większa złożoność tej jednej metody. Jeśli zastanawiasz się, czy masz za dużo instrukcji return, możesz zadać sobie pytanie, czy w tej funkcji jest zbyt wiele wierszy kodu.
Ale nie, nie ma nic złego w jednym / wielu zwrotach. W niektórych językach jest to lepsza praktyka (C ++) niż w innych (C).