Czy powinienem pójść normalną ścieżką, czy wcześnie zawieść?


73

Z książki Code Complete pochodzi następujący cytat:

„Postaw normalną przypadek po, ifa nie po else”

Co oznacza, że ​​należy wprowadzić wyjątki / odchylenia od standardowej ścieżki else.

Ale Pragmatic Programmer uczy nas „wcześnie upaść” (s. 120).

Jakiej zasady powinienem przestrzegać?


15
@gnat not duplicate
— BЈовић

16
oba nie wykluczają się wzajemnie, nie ma dwuznaczności.
— jwenting

6
Nie jestem pewien, czy cytat z pełnym kodem jest świetną radą. Przypuszczalnie jest to próba zwiększenia czytelności, ale z pewnością istnieją sytuacje, w których niecodzienne przypadki powinny być pierwsze tekstowo. Zauważ, że to nie jest odpowiedź; istniejące odpowiedzi już wyjaśniają zamieszanie wynikające z twojego pytania.
— Eamon Nerbonne

14
Crash wcześnie, crash mocno! Jeśli jeden z ifoddziałów powróci, użyj go w pierwszej kolejności. I unikaj elsereszty kodu, już zwróciłeś, jeśli warunki wstępne zawiodły. Kod jest łatwiejszy do odczytania, mniej wcięć ...
— CodeAngry

5
Czy to tylko ja uważam, że nie ma to nic wspólnego z czytelnością, ale raczej była to błędna próba optymalizacji pod kątem przewidywania gałęzi statycznych?
— Mehrdad

Odpowiedzi:


189

„Crash wcześnie” nie dotyczy tego, który wiersz kodu pojawia się wcześniej tekstowo. Informuje o wykrywaniu błędów na jak najwcześniejszym etapie przetwarzania , abyś nieumyślnie nie podejmował decyzji ani nie dokonywał obliczeń w oparciu o już uszkodzony stan.

W konstrukcie if/ elsewykonywany jest tylko jeden blok, więc nie można powiedzieć, że żaden z nich stanowi krok „wcześniejszy” lub „późniejszy”. To, jak je zamówić, jest zatem kwestią czytelności, a „wczesne niepowodzenie” nie wchodzi w grę.


2
Twoja ogólna uwaga jest świetna, ale widzę jeden problem. Jeśli masz (if / else if / else), to wyrażenie wyrażone w „else if” jest obliczane po wyrażeniu w instrukcji „if”. Ważnym elementem, jak zauważyłeś, jest czytelność vs. koncepcyjna jednostka przetwarzania. Wykonywany jest tylko jeden blok kodu, ale można ocenić wiele warunków.
— Encaitar

7
@Encaitar, ten poziom szczegółowości jest znacznie mniejszy niż jest to na ogół zamierzone, gdy używa się wyrażenia „wczesne niepowodzenie”.
— riwalk

2
@Encaitar To sztuka programowania. W obliczu wielu kontroli, pomysł polega na wypróbowaniu tego, który najprawdopodobniej będzie prawdziwy. Informacje te mogą jednak nie być znane w czasie projektowania, ale na etapie optymalizacji, ale należy uważać na przedwczesną optymalizację.
— BPugh

Uczciwe komentarze. To była dobra odpowiedź i chciałem tylko pomóc, aby była lepsza na przyszłość
— Encaitar

Języki skryptowe, takie jak JavaScript, Python, perl, PHP, bash itp. Są wyjątkami, ponieważ są interpretowane liniowo. W małych if/elsekonstrukcjach prawdopodobnie nie ma to znaczenia. Ale te wywoływane w pętli lub z wieloma instrukcjami w każdym bloku mogą działać szybciej z najczęściej występującym warunkiem.
— DocSalvager

116

Jeśli twoje elseoświadczenie zawiera tylko kod błędu, najprawdopodobniej nie powinno go tam być.

Zamiast tego:

if file.exists() :
  if validate(file) :
    # do stuff with file...
  else :
    throw foodAtMummy
else :
  throw toysOutOfPram

Zrób to

if not file.exists() :
  throw toysOutOfPram

if not validate(file) :
  throw foodAtMummy

# do stuff with file...

Nie chcesz głęboko zagnieżdżać kodu, aby po prostu włączyć sprawdzanie błędów.

I jak wszyscy inni już stwierdzili, te dwie rady nie są sprzeczne. Jeden dotyczy kolejności wykonywania , drugi dotyczy kolejności kodu .


4
Warto wyraźnie powiedzieć, że porada, aby umieścić normalny przepływ w bloku po ifi wyjątkowy przepływ w bloku po, elsenie ma zastosowania, jeśli nie masz else! Takie instrukcje ochronne są preferowaną formą obsługi warunków błędów w większości stylów kodowania.
— Jules

+1 jest to dobry punkt i faktycznie stanowi odpowiedź na prawdziwe pytanie, jak zamówić rzeczy z warunkami błędu.
— ashes999

Zdecydowanie dużo jaśniejsze i łatwiejsze w utrzymaniu. Tak lubię to robić.
— John

27

Powinieneś śledzić oba z nich.

Porada „Wcześniejsza awaria” / „Nieudana wczesna” oznacza, że ​​należy jak najszybciej przetestować dane wejściowe pod kątem możliwych błędów.
Na przykład, jeśli twoja metoda przyjmuje rozmiar lub liczbę, która powinna być dodatnia (> 0), to wczesna porada „fail” oznacza, że ​​testujesz ten warunek na początku metody, zamiast czekać na algorytm, który wygeneruje bzdury wyniki.

Porada, jak postawić normalny przypadek na pierwszym miejscu, oznacza, że ​​jeśli testujesz na warunek, najbardziej prawdopodobna ścieżka powinna być na pierwszym miejscu. Pomaga to w wydajności (ponieważ przewidywania gałęzi procesora częściej będą poprawne) i czytelności, ponieważ nie musisz pomijać bloków kodu, gdy próbujesz dowiedzieć się, co robi funkcja w normalnym przypadku.
Ta rada tak naprawdę nie ma zastosowania podczas testowania warunku wstępnego i natychmiastowego ratowania (za pomocą twierdzeń lub if (!precondition) throwkonstrukcji), ponieważ nie ma obsługi błędów, które można pominąć podczas czytania kodu.


1
Czy potrafisz rozwinąć część dotyczącą przewidywania gałęzi? Nie spodziewałbym się, że kod, który częściej przechodzi do przypadku if, działa szybciej niż kod, który częściej przechodzi do przypadku else. Chodzi mi o to, że o to chodzi w przewidywaniu gałęzi, prawda?
— Roman Reiner,

@ user136712: W nowoczesnych (szybkich) procesorach instrukcje są pobierane przed zakończeniem przetwarzania poprzedniej instrukcji. Prognozowanie rozgałęzień służy do zwiększenia prawdopodobieństwa, że ​​instrukcje pobrane podczas wykonywania rozgałęzienia warunkowego są również odpowiednimi instrukcjami do wykonania.
— Bart van Ingen Schenau

2
Wiem, co to jest prognoza gałęzi. Jeśli dobrze przeczytam twój post, mówisz, że if(cond){/*more likely code*/}else{/*less likely code*/}działa szybciej niż z if(!cond){/*less likely code*/}else{/*more likely code*/}powodu przewidywania gałęzi. Sądzę, że przewidywanie gałęzi nie jest tendencyjne ani do instrukcji, ifani do elseoświadczenia i uwzględnia jedynie historię. Więc jeśli elsejest bardziej prawdopodobne, powinno być w stanie przewidzieć to równie dobrze. Czy to założenie jest fałszywe?
— Roman Reiner

18

Myślę, że @JackAidley powiedział już o tym , ale pozwólcie, że sformułuję to w następujący sposób:

bez wyjątków (np. C)

W zwykłym przepływie kodu masz:

if (condition) {
    statement;
} else if (less_likely_condition) {
    less_likely_statement;
} else {
    least_likely_statement;
}
more_statements;

W przypadku „wczesnego błędu” kod nagle wyświetla:

/* demonstration example, do NOT code like this */
if (condition) {
    statement;
} else {
    error_handling;
    return;
}

Jeśli zauważysz ten wzorzec - returnw bloku else(a nawet if), przerób go natychmiast, aby kod nie zawierał elsebloku:

/* only code like this at University, to please structured programming professors */
function foo {
    if (condition) {
        lots_of_statements;
    }
    return;
}

W prawdziwym świecie…

/* code like this instead */
if (!condition) {
    error_handling;
    return;
}
lots_of_statements;

Pozwala to uniknąć zbyt głębokiego zagnieżdżenia i spełnia warunek „wczesnego wybicia” (pomaga utrzymać umysł - i przepływ kodu - w czystości) i nie narusza „wstawienia bardziej prawdopodobnej rzeczy do ifelementu”, ponieważ po prostu nie ma elseczęści .

C i porządki

Zainspirowany odpowiedzią na podobne pytanie (które popełniło błąd), oto sposób czyszczenia w C. Możesz tam użyć jednego lub dwóch punktów wyjścia, tutaj jest jeden dla dwóch punktów wyjścia:

struct foo *
alloc_and_init(size_t arg1, int arg2)
{
    struct foo *res;

    if (!(res = calloc(sizeof(struct foo), 1)))
        return (NULL);

    if (foo_init1(res, arg1))
        goto err;
    res.arg1_inited = true;
    if (foo_init2(&(res->blah), arg2))
        goto err;
    foo_init_complete(res);
    return (res);

 err:
    /* safe because we use calloc and false == 0 */
    if (res.arg1_inited)
        foo_dispose1(res);
    free(res);
    return (NULL);
}

Możesz zwinąć je w jednym punkcie wyjścia, jeśli jest mniej do zrobienia:

char *
NULL_safe_strdup(const char *arg)
{
    char *res = NULL;

    if (arg == NULL)
        goto out;

    /* imagine more lines here */
    res = strdup(arg);

 out:
    return (res);
}

Takie użycie gotojest w porządku, jeśli sobie z tym poradzisz; rada, aby nie używać, gotojest skierowana do osób, które nie mogą jeszcze samodzielnie zdecydować, czy użycie jest dobre, dopuszczalne, złe, kod spaghetti, czy coś innego.

Wyjątki

Powyższe mówi o językach bez wyjątków, które bardzo wolę siebie (mogę użyć jawnej obsługi błędów znacznie lepiej i ze znacznie mniejszym zaskoczeniem). Aby zacytować igli:

<igli> exceptions: a truly awful implementation of quite a nice idea.
<igli> just about the worst way you could do something like that, afaic.
<igli> it's like anti-design.
<mirabilos> that too… may I quote you on that?
<igli> sure, tho i doubt anyone will listen ;)

Ale oto sugestia, jak to zrobić dobrze w języku z wyjątkami i kiedy chcesz je dobrze wykorzystać:

błąd powrotu w obliczu wyjątków

Możesz zastąpić większość wczesnych returns rzucając wyjątek. Jednakże , twój normalny przepływ programu, czyli dowolnego strumienia kodu, w którym program nie napotka, dobrze, wyjątek ... stan błędu lub somesuch, nie powinna podnosić żadnych wyjątków.

To znaczy że…

# this page is only available to logged-in users
if not isLoggedIn():
    # this is Python 2.5 style; insert your favourite raise/throw here
    raise "eh?"

… Jest w porządku, ale…

/* do not code like this! */
try {
    openFile(xyz, "rw");
} catch (LockedException e) {
    return "file is locked";
}
closeFile(xyz);
return "file is not locked";

… nie jest. Zasadniczo wyjątek nie jest elementem kontroli . To sprawia, że ​​Operacje wyglądają na ciebie dziwnie („ci programiści Java ™ zawsze mówią nam, że te wyjątki są normalne”) i mogą utrudniać debugowanie (np. Powiedzieć IDE, aby po prostu złamało dowolny wyjątek). Wyjątki często wymagają środowiska wykonawczego, aby rozwinąć stos w celu wygenerowania śledzenia itp. Prawdopodobnie jest więcej powodów.

Sprowadza się to do: w języku wspierającym wyjątki, używaj tego, co pasuje do istniejącej logiki i stylu i wydaje się naturalne. Jeśli piszesz coś od zera, uzgodnij to wcześniej. Pisząc bibliotekę od podstaw, pomyśl o swoich konsumentach. (Nigdy nie używaj abort()w bibliotece…) Ale cokolwiek zrobisz, nie z reguły rzucaj wyjątek, jeśli operacja będzie kontynuowana (mniej więcej) normalnie po nim.

ogólne porady wrt. Wyjątki

Postaraj się najpierw wykorzystać wszystkie wyjątki w programie uzgodnione przez cały zespół programistów. Zasadniczo je zaplanuj. Nie używaj ich w obfitości. Czasami nawet w C ++, Java ™, Python powrót błędu jest lepszy. Czasami tak nie jest; używaj ich z myślą.


Ogólnie rzecz biorąc, wczesne zwroty widzę jako zapach kodu. Zamiast tego rzuciłbym wyjątek, jeśli poniższy kod się nie powiedzie, ponieważ nie został spełniony warunek wstępny. Tylko
— mówię

1
@ DanMan: mój artykuł został napisany z myślą o C… Zwykle nie używam wyjątków. Ale rozszerzyłem artykuł o (ups, ma dość długą) sugestię wrt. Wyjątki; nawiasem mówiąc, mieliśmy to samo pytanie na wewnętrznej liście dyskusyjnej
— deweloperów firmy

Używaj również nawiasów klamrowych nawet w jednowierszowych ifs i forach. Nie chcesz innego goto fail;ukrytego w tożsamości.
— Bruno Kim

1
@BrunoKim: zależy to w pełni od stylu / konwencji kodowania projektu, z którym pracujesz. Pracuję z BSD, gdzie jest to niezadowolone (więcej bałaganu optycznego i utrata przestrzeni pionowej); w $ dayjob jednak umieszczam je zgodnie z ustaleniami (mniej trudne dla początkujących, mniejsza szansa na błędy itp., jak powiedziałeś).
— mirabilos

3

Moim zdaniem „Warunek ochrony” jest jednym z najlepszych i najłatwiejszych sposobów na odczyt kodu. Naprawdę nienawidzę, gdy widzę ifna początku metody i nie widzę elsekodu, ponieważ jest on poza ekranem. Muszę przewinąć w dół, żeby zobaczyć throw new Exception.

Umieść kontrole na początku, aby osoba czytająca kod nie musiała przeskakiwać całej metody, aby ją przeczytać, ale zawsze skanuj ją od góry do dołu.


2

( Odpowiedź @mirabilos jest doskonała, ale oto, jak myślę o pytaniu, aby dojść do tego samego wniosku :)

Myślę o sobie (lub kimś innym) czytającym kod mojej funkcji później. Kiedy czytam pierwszy wiersz, nie mogę poczynić żadnych założeń na temat moich danych wejściowych (poza tymi, których i tak nie sprawdzę). Więc myślę: „Ok, wiem, że będę robił różne rzeczy z moimi argumentami, ale najpierw„ wyczyśćmy je ”- tzn. Zabijaj ścieżki kontrolne, w których nie są one w moich upodobaniach.” Ale jednocześnie , Nie widzę normalnego przypadku jako czegoś, co jest uwarunkowane; chcę podkreślić, że jest normalny.

int foo(int* bar, int baz) {

   if (bar == NULL) /* I don't like you, leave me alone */;
   if (baz < 0) /* go away */;

   /* there, now I can do the work I came into this function to do,
      and I can safely forget about those if's above and make all 
      the assumptions I like. */

   /* etc. */
}

-3

Tego rodzaju porządkowanie warunków zależy od krytyczności omawianej sekcji kodu i tego, czy można zastosować wartości domyślne.

Innymi słowy:

A. sekcja krytyczna i brak wartości domyślnych => wczesne niepowodzenie

B. sekcja niekrytyczna i wartości domyślne => Użyj wartości domyślnych w innej części

C. pomiędzy przypadkami => w razie potrzeby decyduj o każdej sprawie


czy to tylko Twoja opinia, czy możesz to jakoś wyjaśnić / poprzeć?
— komar

jak to dokładnie nie jest zabezpieczone, ponieważ każda opcja wyjaśnia (bez wielu słów) dlaczego jest używana?
— Nikos M.,

nie chciałbym tego mówić, ale opinie negatywne w (mojej) tej odpowiedzi są poza kontekstem :). to jest pytanie zadane przez OP, czy masz alternatywną odpowiedź, to zupełnie inna sprawa
— Nikos M.

Naprawdę nie widzę tu wyjaśnienia. Powiedzmy, że jeśli ktoś napisze inną opinię, na przykład „sekcja krytyczna i brak wartości domyślnych => nie zawiedź na początku” , w jaki sposób ta odpowiedź pomoże czytelnikowi wybrać dwie przeciwne opinie? Rozważ edycję go w lepszym kształcie, aby pasował do wskazówek dotyczących odpowiedzi .
— komar

ok, rozumiem, to rzeczywiście może być inne wytłumaczenie, ale przynajmniej rozumiesz słowo „sekcja krytyczna” i „brak domyślnych”, które mogą sugerować strategię wczesnego niepowodzenia i jest to rzeczywiście wyjaśnienie, choć minimalistyczne
— Nikos M.
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.