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ć?

jao
źródło
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ę.

Kilian Foth
źródło
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 .

Jack Aidley
źródło
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.

Bart van Ingen Schenau
źródło
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ą.

mirabilos
źródło
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.

Piotr Perak
źródło
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. */
}
einpoklum
źródło
-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

Nikos M.
źródło
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.