Często rozmawiam z programistami, którzy mówią: „ Nie umieszczaj wielu instrukcji return w tej samej metodzie ” . Gdy pytam ich o powody, otrzymuję tylko „ Standard kodowania tak mówi ” lub „ To mylące ”. Kiedy pokazują mi rozwiązania z pojedynczą instrukcją return, kod wygląda dla mnie brzydiej. Na przykład:
if (condition)
return 42;
else
return 97;
„ To brzydkie, musisz użyć zmiennej lokalnej! ”
int result;
if (condition)
result = 42;
else
result = 97;
return result;
W jaki sposób ten 50% wzdęcie kodu sprawia, że program jest łatwiejszy do zrozumienia? Osobiście uważam to za trudniejsze, ponieważ przestrzeń stanu właśnie wzrosła o kolejną zmienną, której można było łatwo zapobiec.
Oczywiście normalnie po prostu napisałbym:
return (condition) ? 42 : 97;
Ale wielu programistów unika operatora warunkowego i preferuje długą formę.
Skąd wzięło się pojęcie „tylko jeden powrót”? Czy istnieje historyczny powód, dla którego powstała ta konwencja?
źródło
Odpowiedzi:
„Single Entry, Single Exit” zostało napisane, gdy większość programowania została wykonana w języku asemblera, FORTRAN lub COBOL. Został on źle zinterpretowany, ponieważ współczesne języki nie wspierają praktyk, przed którymi ostrzegała Dijkstra.
„Pojedyncze wejście” oznacza „nie twórz alternatywnych punktów wejścia dla funkcji”. Oczywiście w języku asemblera można wprowadzić funkcję w dowolnej instrukcji. FORTRAN obsługiwał wiele pozycji funkcji za pomocą
ENTRY
instrukcji:„Pojedyncze wyjście” oznaczało, że funkcja powinna powrócić tylko do jednego miejsca: instrukcji bezpośrednio po wywołaniu. To było nie oznacza to, że funkcja powinna zwracać tylko z jednego miejsca. Kiedy pisano programowanie strukturalne , powszechną praktyką było wskazywanie błędu przez funkcję poprzez powrót do innej lokalizacji. FORTRAN wspierał to poprzez „alternatywny zwrot”:
Obie te techniki były wysoce podatne na błędy. Użycie alternatywnych wpisów często pozostawiało niektóre zmienne niezainicjowane. Zastosowanie alternatywnych zwrotów miało wszystkie problemy z instrukcją GOTO, z dodatkową komplikacją, że warunek rozgałęzienia nie sąsiadował z rozgałęzieniem, ale gdzieś w podprogramie.
źródło
const
zanim wielu użytkowników tutaj się urodziło, więc nie ma już potrzeby stosowania stałych kapitałowych Ale nawet w C. Java zachowane wszystkie te złe starych nawyków C .setjmp/longjmp
)Pojęcie pojedynczego wejścia, pojedynczego wyjścia (SESE) pochodzi z języków z jawnym zarządzaniem zasobami , takich jak C i asembler. W języku C taki kod spowoduje wyciek zasobów:
W takich językach masz zasadniczo trzy opcje:
Replikuj kod czyszczenia.
Ugh. Redundancja jest zawsze zła.
Użyj a,
goto
aby przejść do kodu czyszczenia.Wymaga to, aby kod czyszczenia był ostatnią rzeczą w funkcji. (I dlatego niektórzy twierdzą, że
goto
ma swoje miejsce. I rzeczywiście - w C.)Wprowadź zmienną lokalną i manipuluj przez nią przepływem sterowania.
Wadą jest to, że przepływ sterowania manipulowane przez składni (myślę
break
,return
,if
,while
) jest o wiele łatwiejsze do naśladowania niż przepływ sterowania manipulowane przez stan zmiennych (bo te nie mają zmienne stanu, jeśli spojrzeć na algorytmie).W asemblerze jest jeszcze dziwniej, ponieważ możesz wywołać dowolny adres w funkcji, gdy wywołujesz tę funkcję, co oznacza, że masz prawie nieograniczoną liczbę punktów wejścia do dowolnej funkcji. (Czasami jest to pomocne. Takie zespoły są powszechną techniką dla kompilatorów do implementacji
this
dostosowania wskaźnika koniecznego do wywoływaniavirtual
funkcji w scenariuszach wielokrotnego dziedziczenia w C ++.)Gdy trzeba ręcznie zarządzać zasobami, korzystanie z opcji wprowadzania lub wychodzenia z funkcji w dowolnym miejscu prowadzi do bardziej złożonego kodu, a tym samym do błędów. Dlatego pojawiła się szkoła myślenia, która propagowała SESE, aby uzyskać czystszy kod i mniej błędów.
Jednak, gdy język zawiera wyjątki, (prawie) dowolna funkcja może zostać przedwcześnie zakończona (prawie) w dowolnym momencie, więc i tak należy przewidzieć możliwość przedwczesnego powrotu. (Myślę, że
finally
jest głównie używany do tego w Javie iusing
(podczas implementacjiIDisposable
, wfinally
przeciwnym razie) w C #; C ++ zamiast tego wykorzystuje RAII .) Po wykonaniu tej czynności nie można nie posprzątać po sobie z powodu wczesnegoreturn
oświadczenia, więc co jest prawdopodobnie najsilniejszy argument przemawiający za SESE zniknął.To pozostawia czytelność. Oczywiście funkcja 200 LoC z
return
losowo posypanymi pół tuzinem instrukcji nie jest dobrym stylem programowania i nie zapewnia czytelnego kodu. Ale taka funkcja nie byłaby łatwa do zrozumienia bez tych przedwczesnych zwrotów.W językach, w których zasoby nie są lub nie powinny być zarządzane ręcznie, przestrzeganie starej konwencji SESE ma niewielką lub żadną wartość. OTOH, jak argumentowałem powyżej, SESE często czyni kod bardziej złożonym . To dinozaur, który (oprócz C) nie pasuje dobrze do większości współczesnych języków. Zamiast pomagać w zrozumieniu kodu, utrudnia go.
Dlaczego programiści Java trzymają się tego? Nie wiem, ale z mojego (zewnętrznego) POV Java wzięła wiele konwencji z C (gdzie mają sens) i zastosowała je do swojego świata OO (gdzie są bezużyteczne lub wręcz złe), gdzie teraz się trzyma im, bez względu na koszty. (Podobna konwencja do definiowania wszystkich zmiennych na początku zakresu).
Programiści trzymają się różnego rodzaju dziwnych zapisów z irracjonalnych powodów. (Głęboko zagnieżdżone instrukcje strukturalne - „groty strzał” - były w takich językach jak Pascal, kiedyś postrzegane jako piękny kod.) Zastosowanie czystego logicznego rozumowania wydaje się nie przekonać większości z nich do odstąpienia od ustalonych sposobów. Najlepszym sposobem na zmianę takich nawyków jest prawdopodobnie nauczenie ich wcześnie, jak robić to, co najlepsze, a nie to, co konwencjonalne. Jako nauczyciel programowania masz go w ręku.
:)
źródło
finally
klauzul, w których jest wykonywany niezależnie od wczesnychreturn
wyjątków lub wyjątków.finally
większości czasu.malloc()
ifree()
jako komentarz), mówiłem ogólnie o zasobach. Nie sugerowałem też, że GC rozwiąże te problemy. (Wspomniałem o C ++, który nie ma GC po wyjęciu z pudełka.) Z tego, co rozumiem, Javafinally
jest używana do rozwiązania tego problemu.Z jednej strony pojedyncze instrukcje zwrotne ułatwiają rejestrowanie, a także formy debugowania oparte na logowaniu. Pamiętam, że wiele razy musiałem zredukować funkcję do pojedynczego zwrotu, aby wydrukować wartość zwracaną w jednym punkcie.
Z drugiej strony, możesz to przełożyć na
function()
te połączenia_function()
i rejestrować wynik.źródło
_function()
,return
zs w odpowiednich miejscach, i opakowanie o nazwie,function()
które obsługuje zewnętrzne rejestrowanie, niż miećfunction()
logikę ze zniekształconą logiką, aby wszystkie zwroty pasowały do jednego wyjścia -point tylko po to, abym mógł wstawić dodatkową instrukcję przed tym punktem.„Single Entry, Single Exit” wywodzi się z rewolucji programowania strukturalnego z początku lat 70. XX wieku, którą zainicjował list Edsgera W. Dijkstry do redakcji „ Oświadczenie GOTO uznane za szkodliwe ”. Koncepcje programowania strukturalnego zostały szczegółowo opisane w klasycznej książce „Structured Programming” autorstwa Ole Johan-Dahla, Edsgera W. Dijkstry i Charlesa Anthony'ego Richarda Hoare'a.
„Oświadczenie GOTO uznane za szkodliwe” jest wymagane nawet dziś. „Programowanie strukturalne” jest przestarzałe, ale wciąż bardzo, bardzo satysfakcjonujące i powinno znajdować się na szczycie listy „Must Read” każdego programisty, znacznie przewyższającej wszystko np. Steve'a McConnella. (Sekcja Dahla przedstawia podstawy klas w Simula 67, które są techniczną podstawą dla klas w C ++ i całego programowania obiektowego.)
źródło
goto
dosłownie można było przejść gdziekolwiek , na przykład do jakiegoś losowego punktu w innej funkcji, omijając wszelkie procedury, funkcje, stos wywołań itp. Żaden rozsądny język nie pozwala na to, aby w dzisiejszych czasach było to prostegoto
. Csetjmp
/longjmp
to jedyny częściowo wyjątkowy przypadek, o którym wiem, i nawet to wymaga współpracy z obu stron. (Częściowo ironiczne, że użyłem tam słowa „wyjątkowy”, biorąc pod uwagę, że wyjątki robią prawie to samo…) Zasadniczo artykuł zniechęca do praktyki, która już dawno nie żyła.Zawsze łatwo jest połączyć Fowler.
Jednym z głównych przykładów sprzecznych z SESE są klauzule ochronne:
Zastąp zagnieżdżone warunkowe klauzulami ochronnymi
źródło
_isSeparated
i_isRetired
może to być prawda (i dlaczego nie byłoby to możliwe?) Zwracasz niewłaściwą kwotę.Jakiś czas temu napisałem blog na ten temat.
Najważniejsze jest to, że ta reguła pochodzi z epoki języków, które nie mają funkcji czyszczenia pamięci ani obsługi wyjątków. Nie ma formalnego badania, które wykazałoby, że ta zasada prowadzi do lepszego kodu we współczesnych językach. Możesz go zignorować, gdy tylko doprowadzi to do skrócenia lub zwiększenia czytelności kodu. Ludzie z Javy, którzy nalegają na to, są ślepi i niekwestionowani zgodnie z przestarzałą, bezcelową zasadą.
To pytanie zostało również zadane na Stackoverflow
źródło
Jeden zwrot ułatwia refaktoryzację. Spróbuj wykonać „metodę wyodrębniania” do wewnętrznej części pętli for, która zawiera zwrot, przerwanie lub kontynuowanie. To się nie powiedzie, ponieważ przerwałeś kontrolę.
Chodzi o to: Myślę, że nikt nie udaje, że pisze idealny kod. Dlatego kodowanie jest regularnie poddawane refaktoryzacji w celu „ulepszenia” i rozszerzenia. Więc moim celem byłoby, aby mój kod był jak najbardziej przyjazny dla refaktoryzacji.
Często mam problem z tym, że muszę całkowicie przeformułować funkcje, jeśli zawierają one wyłączniki kontroli przepływu i jeśli chcę dodać tylko niewielką funkcjonalność. Jest to bardzo podatne na błędy, ponieważ zmieniasz cały przepływ sterowania zamiast wprowadzać nowe ścieżki do izolowanych zagnieżdżeń. Jeśli masz tylko jeden zwrot na końcu lub używasz strażników do wyjścia z pętli, to oczywiście masz więcej zagnieżdżania i więcej kodu. Ale zyskujesz możliwości refaktoryzacji obsługiwane przez kompilator i IDE.
źródło
Weź pod uwagę fakt, że wiele instrukcji zwrotu jest równoważnych posiadaniu GOTO z jedną instrukcją zwrotu. To samo dotyczy instrukcji break. Dlatego niektórzy, podobnie jak ja, uważają je za GOTO dla wszystkich celów i celów.
Jednak nie uważam tych rodzajów GOTO za szkodliwe i nie zawaham się użyć rzeczywistego GOTO w moim kodzie, jeśli znajdę dobry powód.
Zasadą ogólną jest to, że GOTO służą wyłącznie do kontroli przepływu. Nigdy nie należy ich używać do zapętlania i nigdy nie należy GOTOWAĆ „w górę” lub „w tył”. (tak działają przerwy / zwroty)
Jak wspomnieli inni, poniżej należy przeczytać oświadczenie GOTO uważane za szkodliwe.
Należy jednak pamiętać, że zostało to napisane w 1970 r., Kiedy GOTO były nadmiernie nadużywane. Nie każde GOTO jest szkodliwe i nie odradzałbym ich używania, dopóki nie użyjesz ich zamiast normalnych konstrukcji, ale raczej w dziwnym przypadku, gdy użycie normalnych konstrukcji byłoby bardzo niewygodne.
Uważam, że używanie ich w przypadkach błędów, w których musisz uciec z obszaru z powodu awarii, która nigdy nie powinna wystąpić w normalnych przypadkach, czasami jest przydatna. Ale powinieneś również rozważyć umieszczenie tego kodu w osobnej funkcji, abyś mógł po prostu wrócić wcześniej zamiast używać GOTO ... ale czasami jest to również niewygodne.
źródło
Złożoność cykliczna
Widziałem, że SonarCube używa wielu instrukcji return do określania złożoności cyklicznej. Im więcej instrukcji return, tym wyższa złożoność cykliczna
Zmień typ zwrotu
Wiele zwrotów oznacza, że musimy zmienić w wielu miejscach funkcji, gdy zdecydujemy się zmienić typ zwrotu.
Wiele wyjść
Trudniej jest debugować, ponieważ logika musi być dokładnie przestudiowana w połączeniu z instrukcjami warunkowymi, aby zrozumieć, co spowodowało zwróconą wartość.
Refaktoryzowane rozwiązanie
Rozwiązaniem wielu instrukcji return jest zastąpienie ich polimorfizmem z jednym zwrotem po rozstrzygnięciu wymaganego obiektu implementacji.
źródło