Odwróć instrukcję „if”, aby zmniejszyć zagnieżdżanie


272

Kiedy uruchomiłem ReSharper na moim kodzie, na przykład:

    if (some condition)
    {
        Some code...            
    }

ReSharper dał mi powyższe ostrzeżenie (polecenie Invert „if”, aby zmniejszyć zagnieżdżanie) i zasugerował następującą korektę:

   if (!some condition) return;
   Some code...

Chciałbym zrozumieć, dlaczego tak jest lepiej. Zawsze myślałem, że użycie „return” w środku metody problematycznej, trochę jak „goto”.


1
Uważam, że sprawdzanie i zwracanie wyjątku na początku jest w porządku, ale zmieniłbym warunek, więc sprawdzasz wyjątek bezpośrednio, a nie coś (tj. Jeśli (jakieś warunki) powrócą).
bruceatk,

36
Nie, nie zrobi nic dla wydajności.
Seth Carnegie,

3
Kusiłbym się, by rzucić ArgumentException, jeśli zamiast tego do mojej metody były przekazywane złe dane.
asawyer,

1
@asawyer Tak, tutaj jest cała dyskusja poboczna na temat funkcji, które zbyt wybaczają nonsensowne dane wejściowe - w przeciwieństwie do używania niepowodzenia asercji. Pisanie Solid Code otworzyło mi oczy na to. W takim przypadku byłoby to coś takiego ASSERT( exampleParam > 0 ).
Greg Hendershott,

4
Asercje dotyczą stanu wewnętrznego, a nie parametrów. Rozpocznij od sprawdzenia poprawności parametrów, upewnienia się, że stan wewnętrzny jest poprawny, a następnie wykonania operacji. W kompilacji wydania można pominąć twierdzenia lub odwzorować je na zamknięcie komponentu.
Simon Richter,

Odpowiedzi:


296

Zwrot w środku metody niekoniecznie jest zły. Lepszym rozwiązaniem może być natychmiastowy powrót, jeśli pozwoli to na wyjaśnienie intencji kodu. Na przykład:

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
     return result;
};

W takim przypadku, jeśli _isDeadjest to prawda, możemy natychmiast wyjść z metody. Zamiast tego lepiej byłoby zbudować go w ten sposób:

double getPayAmount() {
    if (_isDead)      return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired)   return retiredAmount();

    return normalPayAmount();
};   

Wybrałem ten kod z katalogu refaktoryzacji . To specyficzne refaktoryzacja nazywa się: Zamień zagnieżdżone warunkowe klauzulami ochronnymi.


13
To naprawdę fajny przykład! Refaktoryzowany kod brzmi bardziej jak instrukcja case.
Poza tym

16
Prawdopodobnie tylko kwestia gustu: proponuję zmienić 2. i 3. „jeśli” na „inne jeśli”, aby jeszcze bardziej zwiększyć czytelność. Jeśli pominięto instrukcję „return”, nadal byłoby jasne, że następujący przypadek jest sprawdzany tylko w przypadku niepowodzenia poprzedniego, tj. Że kolejność kontroli jest ważna.
foraidt

2
W tym prostym przykładzie id zgadza się, że 2. sposób jest lepszy, ale tylko dlatego, że tak oczywiste jest to, co się dzieje.
Andrew Bullock,

Jak teraz wziąć ten piękny kod napisany przez jop i powstrzymać studio wizualne przed zerwaniem i umieszczeniem zwrotów w osobnych wierszach? Naprawdę denerwuje mnie to, że formatuje kod w ten sposób. Sprawia, że ​​ten bardzo czytelny kod jest UGLY.
Mark T

@ Mark T W Visual Studio są ustawienia zapobiegające zerwaniu tego kodu.
Aaron Smith,

333

Jest to nie tylko estetyczne , ale także zmniejsza maksymalny poziom zagnieżdżenia w metodzie. Jest to ogólnie uważane za plus, ponieważ ułatwia zrozumienie metod (a w rzeczywistości wiele narzędzi do analizy statycznej zapewnia to jako jeden ze wskaźników jakości kodu).

Z drugiej strony sprawia, że ​​twoja metoda ma wiele punktów wyjścia, co zdaniem innej grupy osób jest nie-nie.

Osobiście zgadzam się z ReSharper i pierwszą grupą (w języku, który ma wyjątki, głupotą jest omawianie „wielu punktów wyjścia”; prawie wszystko może rzucić, więc istnieje wiele potencjalnych punktów wyjścia we wszystkich metodach).

Jeśli chodzi o wydajność : obie wersje powinny być równoważne (jeśli nie na poziomie IL, to na pewno po zakończeniu fluktuacji kodem) w każdym języku. Teoretycznie zależy to od kompilatora, ale praktycznie każdy powszechnie używany obecnie kompilator jest w stanie obsłużyć znacznie bardziej zaawansowane przypadki optymalizacji kodu niż ten.


41
pojedyncze punkty wyjścia? Kto tego potrzebuje?
sq33G

3
@ sq33G: To pytanie na SESE (i oczywiście odpowiedzi) są fantastyczne. Dzięki za link!
Jon

Ciągle słyszę w odpowiedziach, że są ludzie, którzy opowiadają się za pojedynczymi punktami wyjścia, ale nigdy nie widziałem, żeby ktoś faktycznie to popierał, szczególnie w językach takich jak C #.
Thomas Bonini,

1
@AndreasBonini: Brak dowodu nie jest dowodem nieobecności. :-)
Jon

Tak, oczywiście, po prostu dziwne jest to, że wszyscy odczuwają potrzebę powiedzenia, że ​​niektórzy ludzie lubią inne podejście, jeśli ci ludzie nie czują potrzeby mówienia tego sami =)
Thomas Bonini

102

To trochę religijny argument, ale zgadzam się z ReSharper, że powinieneś preferować mniej zagnieżdżania. Uważam, że przewyższa to negatywne cechy wielu ścieżek powrotnych z funkcji.

Głównym powodem mniejszego zagnieżdżenia jest poprawa czytelności kodu i łatwości konserwacji . Pamiętaj, że wielu innych programistów będzie musiało przeczytać Twój kod w przyszłości, a kod z mniejszymi wcięciami jest ogólnie znacznie łatwiejszy do odczytania.

Warunki wstępne są doskonałym przykładem tego, gdzie można powrócić wcześniej na początku funkcji. Dlaczego na sprawdzenie reszty funkcji powinien mieć wpływ sprawdzanie warunków wstępnych?

Jeśli chodzi o negatywy dotyczące wielokrotnego powrotu z metody - debuggery są teraz dość potężne i bardzo łatwo jest dowiedzieć się, gdzie i kiedy konkretna funkcja powraca.

Posiadanie wielu zwrotów w funkcji nie wpłynie na pracę programisty konserwacji.

Słaba czytelność kodu będzie.


2
Wiele zwrotów w funkcji wpływa na programistę konserwacji. Podczas debugowania funkcji, szukania nieuczciwej wartości zwracanej, opiekun musi umieścić punkty przerwania we wszystkich miejscach, które mogą zwrócić.
EvilTeach,

10
Umieściłem punkt przerwania na otwierającym nawiasie klamrowym. Jeśli przejdziesz przez funkcję, nie tylko zobaczysz, że kontrole są przeprowadzane po kolei, ale będzie całkowicie jasne, które sprawdzenie wylądowała na funkcji dla tego uruchomienia.
John Dunagan,

3
Zgadzam się z Johnem tutaj ... Nie widzę problemu z przejściem przez całą funkcję.
Nailer,

5
@Nailer Bardzo późno, szczególnie się zgadzam, ponieważ jeśli funkcja jest zbyt duża, aby przejść, powinna być podzielona na wiele funkcji!
Aidiakapi

1
Zgadzam się także z Johnem. Być może jest to stara szkoła, która umieszcza punkt przerwania na dole, ale jeśli jesteś ciekawy, co funkcja zwraca, przejdziesz przez tę funkcję, aby zobaczyć, dlaczego i tak zwraca to, co zwraca. Jeśli chcesz tylko zobaczyć, co zwraca, umieść go w ostatnim nawiasie.
user441521

70

Jak wspomnieli inni, nie powinno być hitów wydajnościowych, ale są też inne względy. Oprócz tych ważnych obaw, może to również otworzyć cię na gotchas w niektórych okolicznościach. Załóżmy, że doublezamiast tego masz do czynienia z :

public void myfunction(double exampleParam){
    if(exampleParam > 0){
        //Body will *not* be executed if Double.IsNan(exampleParam)
    }
}

Porównaj to z pozornie równoważną inwersją:

public void myfunction(double exampleParam){
    if(exampleParam <= 0)
        return;
    //Body *will* be executed if Double.IsNan(exampleParam)
}

Więc w pewnych okolicznościach to, co wydaje się być właściwie odwrócone, ifmoże nie być.


4
Należy również zauważyć, że szybka poprawka resharper odwraca przykładParam> 0 na przykładParam <0 nie przykładParam <= 0, co mnie złapało.
nickd

1
I właśnie dlatego ta kontrola powinna być z góry i zwrócona, biorąc pod uwagę, że deweloper uważa, że ​​nan powinna doprowadzić do ratowania.
user441521

51

Pomysł powrotu po zakończeniu funkcji wrócił z dni, kiedy języki miały obsługę wyjątków. Umożliwiło to programom poleganie na możliwości umieszczenia kodu czyszczącego na końcu metody, a następnie upewnienie się, że zostanie wywołany, a inny programista nie ukryłby powrotu w metodzie, która spowodowała pominięcie kodu czyszczenia . Pominięty kod czyszczenia może spowodować wyciek pamięci lub zasobów.

Jednak w języku, który obsługuje wyjątki, nie zapewnia takich gwarancji. W języku obsługującym wyjątki wykonanie dowolnej instrukcji lub wyrażenia może spowodować przepływ sterowania, który spowoduje zakończenie metody. Oznacza to, że czyszczenie musi zostać wykonane przy użyciu słów kluczowych lub słów kluczowych.

W każdym razie mówię, że myślę, że wiele osób powołuje się na wytyczną dotyczącą „jedynego zwrotu na końcu metody”, nie rozumiejąc, dlaczego warto to robić, i że zmniejszenie zagnieżdżania w celu poprawy czytelności jest prawdopodobnie lepszym celem.


6
Właśnie zorientowałeś się, dlaczego wyjątkami są UglyAndEvil [tm] ... ;-) Wyjątek stanowią wymyślne, drogie przebrania.
EricSchaefer,

16
@Eric musiałeś być narażony na wyjątkowo zły kod. Jest to dość oczywiste, gdy są używane nieprawidłowo i zazwyczaj pozwalają na pisanie kodu o wyższej jakości.
Robert Paulson,

Oto odpowiedź, której naprawdę szukałem! Są tutaj świetne odpowiedzi, ale większość z nich koncentruje się na przykładach, a nie na prawdziwym powodzie, dla którego powstało to zalecenie.
Gustavo Mori,

To najlepsza odpowiedź, ponieważ wyjaśnia, dlaczego cały ten argument powstał w pierwszej kolejności.
Buttle Butkus

If you've got deep nesting, maybe your function is trying to do too many things.=> to nie jest poprawna konsekwencja z poprzedniej frazy. Ponieważ tuż przed stwierdzeniem, że można przefakturować zachowanie A z kodem C na zachowanie A z kodem D. kod D jest czystszy, przyznany, ale „zbyt wiele rzeczy” odnosi się do zachowania, które NIE uległo zmianie. Dlatego te wnioski nie mają żadnego sensu.
v.oddou

30

Chciałbym dodać, że istnieje nazwa dla tych odwróconych, jeśli… - Klauzula Strażnicza. Używam go, kiedy tylko mogę.

Nienawidzę czytać kodu tam, gdzie jest, jeśli na początku dwa ekrany kodu i nic więcej. Po prostu odwróć jeśli i wróć. W ten sposób nikt nie będzie tracić czasu na przewijanie.

http://c2.com/cgi/wiki?GuardClause


2
Dokładnie. To bardziej refaktoryzacja. Jak wspomniałeś, pomaga to w czytaniu kodu.
rpattabi

„powrót” nie wystarcza i jest okropny dla klauzuli ochronnej. Zrobiłbym: jeśli (ex <= 0) wyrzuci WrongParamValueEx („[MethodName] Zła wartość parametru wejściowego 1 {0} ... a będziesz musiał złapać wyjątek i zapisać w dzienniku aplikacji
JohnJohnGa

3
@Jan. Masz rację, jeśli to rzeczywiście błąd. Ale często tak nie jest. I zamiast sprawdzać coś w każdym miejscu, w którym wywoływana jest metoda, metoda po prostu sprawdza to i nic nie robi.
Piotr Perak,

3
Nie chciałbym czytać metody, która składa się z dwóch ekranów kodu, kropki, ifs lub nie. lol
jpmc26

1
Ja osobiście wolę wczesne powroty właśnie z tego powodu (także: unikanie zagnieżdżania). Nie ma to jednak związku z pierwotnym pytaniem dotyczącym wydajności i prawdopodobnie powinien to być komentarz.
Patrick M

22

Wpływa nie tylko na estetykę, ale także zapobiega zagnieżdżaniu kodu.

Może faktycznie działać jako warunek wstępny, aby upewnić się, że Twoje dane są również prawidłowe.


18

Jest to oczywiście subiektywne, ale myślę, że zdecydowanie poprawia się w dwóch punktach:

  • Teraz jest od razu oczywiste, że twoja funkcja nie ma już nic do roboty, jeśli zostanie conditionwstrzymana.
  • Utrzymuje poziom zagnieżdżenia w dół. Zagnieżdżanie szkodzi czytelności bardziej niż myślisz.

15

Wiele punktów powrotu stanowiło problem w C (iw mniejszym stopniu C ++), ponieważ zmusiło cię do zduplikowania kodu czyszczenia przed każdym z punktów powrotu. Po wyrzucaniu elementów bezużytecznych try| finallybudować i usingblokować, naprawdę nie ma powodu, aby się ich bać.

Ostatecznie sprowadza się to do tego, co Ty i Twoi koledzy uważacie za łatwiejsze do odczytania.


To nie jedyny powód. Istnieje naukowy powód odnoszący się do pseudo kodu, który nie ma nic wspólnego z praktycznymi względami, takimi jak czyszczenie rzeczy. Ten akamedyczny powód dotyczy poszanowania podstawowych form konstrukcji imperatywnych. Taki sam sposób, w jaki nie umieszczasz wyjścia pętli na środku. W ten sposób niezmienniki mogą być rygorystycznie wykrywane, a zachowanie można udowodnić. Lub wypowiedzenie można udowodnić.
v.oddou

1
aktualności: właściwie znalazłem bardzo praktyczny powód, dla którego wczesna przerwa i powroty są złe, i jest to bezpośrednia konsekwencja tego, co powiedziałem, analizy statycznej. proszę bardzo, przewodnik po kompilatorze Intel C ++: d3f8ykwhia686p.cloudfront.net/1live/intel/… . Fraza kluczowa:a loop that is not vectorizable due to a second data-dependent exit
v.oddou

12

Pod względem wydajności nie będzie zauważalnej różnicy między tymi dwoma podejściami.

Ale kodowanie to coś więcej niż wydajność. Jasność i łatwość konserwacji są również bardzo ważne. I w takich przypadkach, w których nie wpływa to na wydajność, liczy się tylko to.

Istnieją konkurencyjne szkoły myślenia, które podejście jest lepsze.

Jeden widok jest tym, o którym wspominali inni: drugie podejście zmniejsza poziom zagnieżdżenia, co poprawia przejrzystość kodu. Jest to naturalne w imperatywnym stylu: gdy nie masz już nic do roboty, równie dobrze możesz wrócić wcześniej.

Innym poglądem, z perspektywy bardziej funkcjonalnego stylu, jest to, że metoda powinna mieć tylko jeden punkt wyjścia. Wszystko w funkcjonalnym języku jest wyrażeniem. Więc jeśli instrukcje muszą zawsze zawierać klauzule else. W przeciwnym razie wyrażenie if nie zawsze miałoby wartość. Tak więc w funkcjonalnym stylu pierwsze podejście jest bardziej naturalne.


11

Klauzule ochronne lub warunki wstępne (jak zapewne widać) sprawdzają, czy określony warunek jest spełniony, a następnie przerywa działanie programu. Doskonale nadają się do miejsc, w których naprawdę interesuje Cię tylko jeden wynik ifwypowiedzi. Zamiast więc mówić:

if (something) {
    // a lot of indented code
}

Odwracasz warunek i łamiesz się, jeśli ten odwrócony warunek jest spełniony

if (!something) return false; // or another value to show your other code the function did not execute

// all the code from before, save a lot of tabs

returnnie jest tak brudny jak goto. Pozwala przekazać wartość pokazującą resztę kodu, której funkcja nie mogła uruchomić.

Zobaczysz najlepsze przykłady zastosowania tego w warunkach zagnieżdżonych:

if (something) {
    do-something();
    if (something-else) {
        do-another-thing();
    } else {
        do-something-else();
    }
}

vs:

if (!something) return;
do-something();

if (!something-else) return do-something-else();
do-another-thing();

Kilka osób twierdzi, że pierwsza jest czystsza, ale oczywiście całkowicie subiektywna. Niektórzy programiści lubią wiedzieć, w jakich warunkach coś działa przez wcięcia, podczas gdy wolałbym raczej liniowy przepływ metod.

Ani razu nie zasugeruję, że preony zmienią twoje życie lub spowodują, że cię położysz, ale twój kod może być nieco łatwiejszy do odczytania.


6
Chyba jestem wtedy jednym z niewielu. Łatwiej jest mi przeczytać pierwszą wersję. Zagnieżdżone ifs sprawiają, że drzewo decyzyjne staje się bardziej oczywiste. Z drugiej strony, jeśli jest kilka warunków wstępnych, zgadzam się, że lepiej umieścić je wszystkie na górze funkcji.
Poza tym

@Inne strony: całkowicie się zgadzam. Zwroty napisane seryjnie powodują, że Twój mózg musi szeregować możliwe ścieżki. Gdy if-tree można bezpośrednio zamapować na pewną przejściową logikę, na przykład można go skompilować do lisp. ale moda na zwrot szeregowy wymagałaby kompilatora o wiele trudniejszego do zrobienia. Chodzi tutaj oczywiście o to, że nie ma to nic wspólnego z tym hipotetycznym scenariuszem, dotyczy analizy kodu, szans na optymalizację, dowodów korekty, dowodów zakończenia i wykrywania niezmienników.
v.oddou

1
W żaden sposób nikt nie może łatwiej odczytać kodu z większą liczbą gniazd. Im bardziej blok jak coś, tym łatwiej jest go czytać na pierwszy rzut oka. Nie ma mowy, żeby pierwszy przykład był łatwiejszy do odczytania i idzie tylko 2 gniazda, gdy większość takiego kodu w prawdziwym świecie idzie głębiej i przy każdym gnieździe wymaga większej mocy mózgu do naśladowania.
user441521

1
Gdyby zagnieżdżanie było dwa razy głębsze, ile czasu zajęłoby Ci udzielenie odpowiedzi na pytanie: „Kiedy robisz coś innego?” Myślę, że trudno byłoby ci odpowiedzieć bez długopisu i papieru. Ale mógłbyś odpowiedzieć na to dość łatwo, gdyby wszystko było klauzulowane.
David Storfer

9

Jest tu kilka dobrych punktów, ale wiele punktów zwrotnych może być również nieczytelnych , jeśli metoda jest bardzo długa. Biorąc to pod uwagę, jeśli zamierzasz użyć wielu punktów zwrotnych, po prostu upewnij się, że twoja metoda jest krótka, w przeciwnym razie premia za czytelność wielu punktów zwrotnych może zostać utracona.


8

Wydajność składa się z dwóch części. Masz wydajność, gdy oprogramowanie jest w produkcji, ale chcesz też mieć wydajność podczas programowania i debugowania. Ostatnią rzeczą, jakiej chce deweloper, jest „czekanie” na coś trywialnego. Ostatecznie skompilowanie tego z włączoną optymalizacją da podobny kod. Warto więc poznać te małe sztuczki, które się opłacają w obu scenariuszach.

Sprawa w pytaniu jest jasna, ReSharper ma rację. Zamiast zagnieżdżać ifinstrukcje i tworzyć nowy zakres w kodzie, ustawiasz jasną regułę na początku swojej metody. Zwiększa czytelność, będzie łatwiejsza w utrzymaniu i zmniejszy liczbę zasad, które należy przeszukać, aby znaleźć miejsce, do którego chcą się udać.


7

Osobiście wolę tylko 1 punkt wyjścia. Jest to łatwe do osiągnięcia, jeśli twoje metody są krótkie i do rzeczy, i zapewnia przewidywalny wzorzec dla następnej osoby, która pracuje nad twoim kodem.

na przykład.

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

      DataStructure defaultParameters;
      if ((defaultParameters = this.GetApplicationDefaults()) != null)
      {
           succeeded = this.DoSomething(defaultParameters);
      }

      return succeeded;
 }

Jest to również bardzo przydatne, jeśli chcesz tylko sprawdzić wartości niektórych zmiennych lokalnych w funkcji przed jej wyjściem. Wszystko, co musisz zrobić, to umieścić punkt przerwania przy ostatnim powrocie i masz gwarancję, że go trafisz (chyba że zostanie zgłoszony wyjątek).


4
bool PerformDefaultOperation () {DataStructure defaultParameters = this.GetApplicationDefaults (); return (defaultParameters! = NULL && this.DoSomething (defaultParameters);} Tam, naprawiłem to dla ciebie. :)
tchen

1
Ten przykład jest bardzo prosty i nie ma sensu tego wzoru. Dokonaj około 4 innych kontroli wewnątrz tej funkcji, a następnie powiedz nam, że jest bardziej czytelna, ponieważ nie jest.
user441521 24.0416

5

Wiele dobrych powodów, jak wygląda kod . Ale co z wynikami ?

Rzućmy okiem na kod C # i jego skompilowaną formę IL:


using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length == 0) return;
        if ((args.Length+2)/3 == 5) return;
        Console.WriteLine("hey!!!");
    }
}

Ten prosty fragment kodu można skompilować. Możesz otworzyć wygenerowany plik .exe z ildasm i sprawdzić, jaki jest wynik. Nie opublikuję wszystkich elementów asemblera, ale opiszę wyniki.

Wygenerowany kod IL wykonuje następujące czynności:

  1. Jeśli pierwszy warunek jest fałszywy, przeskakuje do kodu, w którym znajduje się drugi.
  2. Jeśli to prawda, przeskakuje do ostatniej instrukcji. (Uwaga: ostatnia instrukcja jest zwrotem).
  3. W drugim warunku to samo dzieje się po obliczeniu wyniku. Porównaj i: dostałem się do Console.WriteLine, jeśli jest fałszywy lub do końca, jeśli to prawda.
  4. Wydrukuj wiadomość i wróć.

Wygląda więc na to, że kod przeskoczy do końca. Co się stanie, jeśli wykonamy normalny kod zagnieżdżony?

using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length != 0 && (args.Length+2)/3 != 5) 
        {
            Console.WriteLine("hey!!!");
        }
    }
}

Wyniki są dość podobne w instrukcjach IL. Różnica polega na tym, że wcześniej istniały skoki dla każdego warunku: jeśli false, przejdź do następnego fragmentu kodu, jeśli true, przejdź do końca. A teraz kod IL płynie lepiej i ma 3 skoki (kompilator nieco to zoptymalizował): 1. Pierwszy skok: gdy Długość wynosi 0 do części, w której kod przeskakuje ponownie (Trzeci skok) do końca. 2. Drugi: w środku drugiego warunku, aby uniknąć jednej instrukcji. 3. Po trzecie: jeśli drugi warunek jest fałszywy, przeskocz na koniec.

W każdym razie licznik programów zawsze będzie skakał.


5
To dobra informacja - ale osobiście nie obchodzi mnie to, że IL „płynie lepiej”, ponieważ ludzie, którzy będą zarządzać kodem, nie zobaczą żadnej IL
JohnIdol

1
Naprawdę nie możesz przewidzieć, jak przebieg optymalizacji w następnej aktualizacji kompilatora C # będzie działał w porównaniu z tym, co widzisz dzisiaj. Osobiście nie spędziłbym ŻADNEGO czasu na ulepszaniu kodu C # w celu uzyskania innego wyniku w IL, chyba że tesing pokazuje, że masz WIELKIE problemy z wydajnością w jakiejś ciasnej pętli i zabrakło Ci innych pomysłów . Nawet wtedy zastanowiłbym się intensywniej nad innymi opcjami. W tym samym duchu wątpię, abyście mieli pojęcie, jakie rodzaje optymalizacji wykonuje kompilator JIT z IL, która jest do niego dostarczana dla każdej platformy ...
Craig

5

Teoretycznie odwracanie ifmoże prowadzić do lepszej wydajności, jeśli zwiększa wskaźnik trafień przewidywania gałęzi. W praktyce myślę, że bardzo trudno jest dokładnie wiedzieć, jak zachowa się przewidywanie gałęzi, szczególnie po kompilacji, więc nie robiłbym tego w codziennym rozwoju, chyba że piszę kod asemblera.

Więcej na temat przewidywania oddziałów tutaj .


4

To jest po prostu kontrowersyjne. Nie ma „porozumienia między programistami” w kwestii wczesnego powrotu. O ile mi wiadomo, jest to zawsze subiektywne.

Można przedstawić argument dotyczący wydajności, ponieważ lepiej jest mieć napisane warunki, aby były one najczęściej prawdziwe; Można również argumentować, że jest to jaśniejsze. Z drugiej strony tworzy testy zagnieżdżone.

Nie sądzę, żebyś uzyskał rozstrzygającą odpowiedź na to pytanie.


Nie rozumiem twojego argumentu dotyczącego wydajności. Warunki są logiczne, więc niezależnie od wyniku, zawsze są dwa wyniki ... Nie rozumiem, dlaczego odwrócenie instrukcji (lub nie) zmieniłoby wynik. (Chyba że mówisz, że dodanie „NIE” do warunku dodaje mierzalną ilość przetwarzania ...
Oli,

2
Optymalizacja działa w ten sposób: Jeśli upewnisz się, że najczęstszym przypadkiem jest blok if zamiast bloku else, wówczas procesor zwykle będzie już miał instrukcje z bloku if załadowane do potoku. Jeśli warunek zwróci wartość false, procesor musi opróżnić swój potok i ...
Poza tym

Lubię też robić to, co mówi druga strona, tylko dla czytelności. Nienawidzę mieć negatywnych przypadków w bloku „następnie” zamiast w „innym”. Sensowne jest robienie tego nawet bez wiedzy i zastanowienia się, co robi procesor
Andrew Bullock,

3

Istnieje już wiele wnikliwych odpowiedzi, ale nadal chciałbym skierować się do nieco innej sytuacji: zamiast warunku wstępnego, który powinien być rzeczywiście nadpisany funkcji, pomyśl o inicjalizacji krok po kroku, w której muszę sprawdzić, czy każdy krok się powiódł, a następnie przejść do następnego. W takim przypadku nie można sprawdzić wszystkiego u góry.

Mój kod był naprawdę nieczytelny podczas pisania aplikacji hosta ASIO za pomocą ASIOSDK Steinberga, ponieważ postępowałem zgodnie z paradygmatem zagnieżdżania. Osiągnęło głębokość ośmiu poziomów i nie widzę tam wady projektowej, o czym wspomniał Andrew Bullock powyżej. Oczywiście mogłem spakować jakiś wewnętrzny kod do innej funkcji, a następnie zagnieździć tam pozostałe poziomy, aby był bardziej czytelny, ale wydaje mi się to dość losowe.

Zastępując zagnieżdżanie klauzulami ochronnymi, odkryłem nawet moje błędne przekonanie dotyczące części kodu czyszczącego, które powinno było nastąpić znacznie wcześniej w funkcji zamiast na końcu. Z zagnieżdżonymi gałęziami nigdy bym tego nie widział, można nawet powiedzieć, że doprowadziły do ​​mojego błędnego przekonania.

Może to być kolejna sytuacja, w której odwrócone ifs mogą przyczynić się do wyraźniejszego kodu.


3

Unikanie wielu punktów wyjścia może prowadzić do wzrostu wydajności. Nie jestem pewien co do C #, ale w C ++ Optymalizacja nazwanych wartości zwrotnych (Copy Elision, ISO C ++ '03 12.8 / 15) zależy od posiadania pojedynczego punktu wyjścia. Ta optymalizacja pozwala uniknąć tworzenia kopii zwracanej przez kopię (w twoim przykładzie nie ma to znaczenia). Może to prowadzić do znacznego wzrostu wydajności w ciasnych pętlach, ponieważ zapisujesz konstruktor i destruktor za każdym razem, gdy funkcja jest wywoływana.

Jednak w 99% przypadków zapisanie dodatkowych wywołań konstruktora i destruktora nie jest warte utraty ifwprowadzonych bloków czytelności (jak zauważyli inni).


2
tam. zajęło mi 3 długie czytanie dziesiątek odpowiedzi w 3 pytaniach zadających to samo (2 z nich zostały zamrożone), aby w końcu znaleźć kogoś, kto wspomina o NRVO. rany ... dziękuję.
v.oddou

2

To kwestia opinii.

Moim normalnym podejściem byłoby unikanie pojedynczej linii ifs i powrót w trakcie metody.

Nie chciałbyś linii takich jak sugeruje to wszędzie w twojej metodzie, ale jest coś do powiedzenia na temat sprawdzania szeregu założeń u góry metody i wykonywania rzeczywistej pracy tylko wtedy, gdy wszystkie przejdą pomyślnie.


2

Moim zdaniem wczesny zwrot jest w porządku, jeśli zwracasz tylko wartość void (lub jakiś bezużyteczny kod powrotu, którego nigdy nie będziesz sprawdzać) i może poprawić czytelność, ponieważ unikasz zagnieżdżania, a jednocześnie wyraźnie zaznaczasz, że funkcja została wykonana.

Jeśli faktycznie zwracasz wartość returnValue - zagnieżdżanie jest zwykle lepszym sposobem, ponieważ zwracasz wartość returnValue tylko w jednym miejscu (na końcu - duh), i może sprawić, że twój kod będzie łatwiejszy w utrzymaniu w wielu przypadkach.


1

Nie jestem pewien, ale myślę, że R # stara się unikać dalekich skoków. Gdy masz IF-ELSE, kompilator robi coś takiego:

Warunek false -> daleko przeskocz do false_condition_label

true_condition_label: instrukcja1 ... instrukcja_n

false_condition_label: instrukcja1 ... instrukcja_n

blok końcowy

Jeśli warunek jest spełniony, nie ma skoku i nie ma rozwijanej pamięci podręcznej L1, ale skok do etykiety fałsz-warunek może być bardzo daleko i procesor musi rozwinąć swoją własną pamięć podręczną. Synchronizacja pamięci podręcznej jest droga. R # próbuje zastąpić dalekie skoki krótkimi skokami, w tym przypadku istnieje większe prawdopodobieństwo, że wszystkie instrukcje są już w pamięci podręcznej.


0

Myślę, że to zależy od tego, co wolisz, jak wspomniano, nie ma ogólnej zgody afaik. Aby zmniejszyć annoyment, możesz ograniczyć tego rodzaju ostrzeżenie do „Podpowiedzi”


0

Mój pomysł jest taki, że zwrot „w środku funkcji” nie powinien być tak „subiektywny”. Powód jest dość prosty, weź ten kod:

    funkcja do_something (data) {

      if (! is_valid_data (data)) 
            zwracać fałsz;


       do_something_that_take_an_hour (data);

       istance = nowy object_with_very_painful_constructor (dane);

          jeśli (istnienie jest nieprawidłowe) {
               Komunikat o błędzie( );
                powrót ;

          }
       connect_to_database ();
       get_some_other_data ();
       powrót;
    }

Może pierwszy „powrót” nie jest TAK intuicyjny, ale to naprawdę oszczędza. Jest zbyt wiele „pomysłów” na czyste kody, które po prostu wymagają więcej praktyki, aby stracić swoje „subiektywne” złe pomysły.


W języku wyjątków te problemy nie występują.
user441521

0

Istnieje kilka zalet tego rodzaju kodowania, ale dla mnie dużą wygraną jest to, że jeśli szybko wrócisz, możesz poprawić szybkość aplikacji. IE Wiem, że dzięki Warunkowi X mogę szybko wrócić z błędem. Najpierw usuwa się przypadki błędów i zmniejsza złożoność kodu. W wielu przypadkach, ponieważ potok procesora może być teraz czystszy, może zatrzymać awarie lub przełączenia potoku. Po drugie, jeśli jesteś w pętli, szybkie zerwanie lub powrót może zaoszczędzić sporo procesora. Niektórzy programiści używają niezmienników pętli do wykonania tego rodzaju szybkiego wyjścia, ale w tym przypadku możesz przerwać procesor procesora, a nawet stworzyć problem z wyszukiwaniem pamięci, co oznacza, że ​​procesor musi zostać załadowany z zewnętrznej pamięci podręcznej. Ale w zasadzie uważam, że powinieneś robić to, co zamierzałeś, to znaczy, że pętla lub funkcja nie tworzą złożonej ścieżki kodu, aby zaimplementować abstrakcyjne pojęcie poprawnego kodu. Jeśli jedynym narzędziem, które masz, jest młotek, wtedy wszystko wygląda jak gwóźdź.


Czytając odpowiedź graffic, wygląda to raczej na to, że zagnieżdżony kod został zoptymalizowany przez kompilator w taki sposób, że wykonywany kod jest szybszy niż użycie wielu zwrotów. Jednak nawet jeśli wielokrotne zwroty były szybsze, ta optymalizacja prawdopodobnie nie przyspieszy tak bardzo twojej aplikacji ... :)
hangy

1
Nie zgadzam się - pierwsze prawo dotyczy poprawnego algorytmu. Ale wszyscy jesteśmy inżynierami, co polega na rozwiązaniu właściwego problemu z dostępnymi zasobami i dostępnym budżetem. Zbyt wolna aplikacja nie nadaje się do określonego celu.
David Allan Finch,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.