Czy należy sprawdzać każdy mały błąd w C?

60

Jako dobry programista powinieneś pisać solidne kody, które będą obsługiwały każdy wynik jego programu. Jednak prawie wszystkie funkcje z biblioteki C zwracają 0 lub -1 lub NULL, gdy wystąpi błąd.

Czasami oczywiste jest, że konieczne jest sprawdzanie błędów, na przykład podczas próby otwarcia pliku. Ale często ignoruję sprawdzanie błędów w funkcjach takich, a printfnawet mallocdlatego, że nie uważam tego za konieczne.

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

Czy dobrą praktyką jest ignorowanie niektórych błędów, czy też istnieje lepszy sposób na obsługę wszystkich błędów?

Derek 朕 會 功夫
źródło
13
Zależy od poziomu niezawodności wymaganego dla projektu, nad którym pracujesz. Systemy, które mają szansę na otrzymywanie danych wejściowych od niezaufanych podmiotów (np. Serwery publiczne) lub działające w nie w pełni zaufanych środowiskach, muszą być kodowane bardzo ostrożnie, aby uniknąć zamieniania się kodu w bombę zegarową (lub zhakowanie najsłabszego łącza ). Oczywiście projekty hobby i uczenia się nie potrzebują takiej solidności.
rwong
1
Niektóre języki przewidują wyjątki. Jeśli nie złapiesz wyjątków, Twój wątek zostanie zakończony, co jest lepsze niż pozwolenie mu na kontynuowanie ze złym stanem. Jeśli zdecydujesz się wychwytywać wyjątki, możesz ukryć wiele błędów w wielu wierszach kodu, w tym wywoływane funkcje i metody za pomocą jednej tryinstrukcji, dzięki czemu nie musisz sprawdzać każdego wywołania lub operacji. (Należy również pamiętać, że niektóre języki są lepsze niż inne w wykrywaniu prostych błędów, takich jak zerowe zerowanie lub indeks tablicy poza zakresem.)
Erik Eidt,
1
Problem ma głównie charakter metodologiczny: zazwyczaj nie piszesz sprawdzania błędów, gdy wciąż zastanawiasz się, co masz zaimplementować i nie chcesz teraz dodawać sprawdzania błędów, ponieważ chcesz uzyskać „szczęśliwą ścieżkę” zaraz pierwszy. Ale nadal powinieneś sprawdzać Malloc i spółkę . Zamiast pomijania kroku sprawdzania błędów, musisz nadać priorytet działaniom związanym z kodowaniem i pozwolić, aby sprawdzanie błędów było domyślnym stałym krokiem refaktoryzacji na liście TODO, stosowanym, gdy tylko będziesz zadowolony z kodu. Dodanie komentarzy greppable „/ * CHECK * /” może pomóc.
coredump
7
Nie mogę uwierzyć, że nikt nie jest wymieniony errno! W przypadku, gdy nie jesteś zaznajomiony, to prawda, że ​​„prawie wszystkie funkcje z biblioteki C zwrócą 0 lub -1 lub NULLgdy wystąpi błąd”, ustawiają również errnozmienną globalną , do której można uzyskać dostęp za pomocą, #include <errno.h>a następnie po prostu czytając wartość errno. Na przykład, jeśli openzwróci (2) -1, możesz sprawdzić, czy to errno == EACCES, co wskazywałoby na błąd uprawnień, lub ENOENT, co wskazywałoby, że żądany plik nie istnieje.
wchargin
1
@ErikEidt C nie obsługuje try/ catch, chociaż można to symulować za pomocą skoków.
Maszt

Odpowiedzi:

29

Zasadniczo kod powinien uwzględniać wyjątkowe warunki tam, gdzie jest to właściwe. Tak, to jest niejasne stwierdzenie.

W językach wyższego poziomu z obsługą wyjątków oprogramowania jest to często określane jako „złap wyjątek w metodzie, w której możesz coś z tym zrobić”. Jeśli wystąpił błąd pliku, być może pozwolisz mu na powiększenie stosu do kodu interfejsu użytkownika, który może faktycznie powiedzieć użytkownikowi „nie udało się zapisać pliku na dysk”. Mechanizm wyjątkowy skutecznie pochłania „każdy mały błąd” i domyślnie obsługuje go w odpowiednim miejscu.

W C nie masz tego luksusu. Istnieje kilka sposobów radzenia sobie z błędami, niektóre z nich są funkcjami języka / biblioteki, niektóre z nich to praktyki kodowania.

Czy dobrą praktyką jest ignorowanie niektórych błędów, czy też istnieje lepszy sposób na obsługę wszystkich błędów?

Zignorować niektóre błędy? Może. Na przykład uzasadnione jest założenie, że zapis na standardowe wyjście nie zakończy się niepowodzeniem. Jeśli to się nie powiedzie, to jak byś powiedział użytkownikowi? Tak, dobrym pomysłem jest ignorowanie niektórych błędów lub kodowanie w celu ich uniknięcia. Na przykład sprawdź zero przed podzieleniem.

Istnieją sposoby obsługi wszystkich lub przynajmniej większości błędów:

  1. Do obsługi błędów można używać skoków, podobnych do gotos . Chociaż jest to sporna kwestia wśród specjalistów od oprogramowania, są one dla nich ważne, szczególnie w kodzie osadzonym i kluczowym dla wydajności (np. Jądro Linuksa).
  1. Kaskadowe ifs:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
    
  2. Przetestuj pierwszy warunek, np. Otwarcie lub utworzenie pliku, a następnie załóż, że kolejne operacje się powiodły.

Solidny kod jest dobry i należy sprawdzać i obsługiwać błędy. To, która metoda jest najlepsza dla twojego kodu, zależy od tego, co robi kod, jak krytyczna jest awaria itp. I tylko Ty możesz naprawdę na to odpowiedzieć. Te metody są jednak testowane w walce i stosowane w różnych projektach typu open source, w których można sprawdzić, jak prawdziwy kod sprawdza błędy.

Społeczność
źródło
21
Niestety, drobne nit do wyboru tutaj - „zapisywanie na standardowe wyjście nie zawiedzie. Jeśli to się nie powiedzie, to jak byś powiedział użytkownikowi?” - pisząc do standardowego błędu? Nie ma gwarancji, że brak zapisu na jednym z nich oznacza, że ​​niemożliwe jest napisanie na drugim.
Damien_The_Unbeliever
4
@Damien_The_Unbeliever - zwłaszcza, że stdoutmożna go przekierować do pliku lub nawet nie istnieć w zależności od systemu. stdoutnie jest strumieniem „zapisu do konsoli”, zwykle tak jest, ale nie musi tak być.
Davor Ždralo,
3
@JAB Wysyłasz wiadomość do stdout... oczywiste :-)
TripeHound,
7
@JAB: Możesz wyjść z EX_IOERR, jeśli to było właściwe.
John Marshall,
6
Cokolwiek robisz, nie bierz tak dalej, jakby nic się nie wydarzyło! Jeśli polecenie dump_database > backups/db_dump.txtnie zapisuje na standardowe wyjście w danym momencie, nie chciałbym, aby kontynuowało się i kończyło pomyślnie. (Nie chodzi o to, że kopie zapasowe baz danych są tworzone w ten sposób, ale kwestia nadal jest ważna)
Ben S
15

Jednak prawie wszystkie funkcje z biblioteki C zwracają 0 lub -1 lub NULL, gdy wystąpi błąd.

Tak, ale wiesz, którą funkcję wywołałeś, prawda?

W rzeczywistości masz wiele informacji, które możesz umieścić w komunikacie o błędzie. Wiesz, która funkcja została wywołana, nazwa funkcji, która ją wywołała, jakie parametry zostały przekazane oraz wartość zwracana przez funkcję. To mnóstwo informacji na bardzo pouczający komunikat o błędzie.

Nie musisz tego robić dla każdego wywołania funkcji. Ale kiedy po raz pierwszy zobaczysz komunikat o błędzie „Wystąpił błąd podczas wyświetlania poprzedniego błędu”, kiedy tak naprawdę potrzebna była użyteczna informacja, będzie to ostatni raz, gdy zobaczysz tam ten komunikat o błędzie, ponieważ natychmiast zamierzasz zmienić komunikat o błędzie informujący o czymś, co pomoże ci rozwiązać problem.

Robert Harvey
źródło
5
Ta odpowiedź sprawiła, że ​​się uśmiechnąłem, bo to prawda, ale nie odpowiada na pytanie.
RubberDuck,
Jak nie odpowiada na pytanie?
Robert Harvey,
2
Jednym z powodów, dla których myślę, że C (i inne języki) pomieszały logiczne 1 i 0, jest ten sam powód, dla którego każda przyzwoita funkcja zwracająca kod błędu zwraca „0” bez żadnego błędu. ale wtedy musisz powiedzieć if (!myfunc()) {/*proceed*/}. 0 nie było błędem, a wszystko niezerowe było jakimś błędem, a każdy rodzaj błędu miał swój własny niezerowy kod. mój przyjaciel powiedział: „jest tylko jedna Prawda, ale wiele kłamstw”. „0” powinno być „prawdą” w if()teście, a wszystko inne niż zero powinno być „fałszem”.
Robert Bristol-Johnson
1
nie, robiąc to po swojemu, istnieje tylko jeden możliwy negatywny kod błędu. i wiele możliwych kodów no_error.
Robert Bristol-Johnson
1
@ robertbristow-johnson: Następnie przeczytaj to jako „Nie wystąpił błąd”. if(function()) { // do something }brzmi „jeśli funkcja zostanie wykonana bez błędu, to zrób coś”. lub jeszcze lepiej „jeśli funkcja zostanie wykonana pomyślnie, zrób coś”. Poważnie, ci, którzy rozumieją konwencję, nietym zdezorientowani. Zwrócenie false(lub 0), gdy wystąpi błąd , nie pozwoli na użycie kodów błędów. Zero oznacza fałsz w C, ponieważ tak działa matematyka. Zobacz programmers.stackexchange.com/q/198284
Robert Harvey,
11

TLDR; prawie nigdy nie należy ignorować błędów.

Język C nie ma dobrej funkcji obsługi błędów, co pozwala programistom na implementację własnych rozwiązań. Bardziej nowoczesne języki mają wbudowane wyjątki, które znacznie ułatwiają obsługę tego konkretnego problemu.

Ale kiedy utkniesz z C, nie masz takich korzyści. Niestety, musisz po prostu zapłacić cenę za każdym razem, gdy wywołujesz funkcję, która jest zdalnie możliwa do awarii. W przeciwnym razie poniesiesz znacznie gorsze konsekwencje, takie jak niezamierzone nadpisywanie danych w pamięci. Dlatego ogólną zasadą jest zawsze sprawdzanie błędów.

Jeśli nie sprawdzisz powrotu fprintf, najprawdopodobniej pozostawisz błąd, który w najlepszym wypadku nie zrobi tego, czego oczekuje użytkownik, a co gorsza, rozbije całą rzecz podczas lotu. Nie ma usprawiedliwienia, aby podważyć się w ten sposób.

Jednak jako programista C Twoim zadaniem jest również łatwe utrzymanie kodu. Czasami więc możesz bezpiecznie zignorować błędy w celu zachowania przejrzystości, jeśli (i tylko wtedy) nie stanowią żadnego zagrożenia dla ogólnego zachowania aplikacji. .

To ten sam problem, co w przypadku:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

Jeśli zauważysz, że bez dobrych komentarzy w pustym bloku catch, jest to z pewnością problem. Nie ma powodu, aby sądzić, że wezwanie malloc()wykona się pomyślnie przez 100% czasu.

Alex
źródło
Zgadzam się, że łatwość konserwacji jest bardzo ważnym aspektem i ten akapit naprawdę odpowiedział na moje pytanie. Dziękuję za szczegółową odpowiedź.
Derek 朕 會 功夫
Myślę, że miliardy programów, które nigdy nie zadają sobie trudu, aby sprawdzić swoje wywołania malloc, a jednak nigdy się nie zawieszają, to dobry powód, aby być leniwym, gdy program komputerowy wykorzystuje tylko kilka MB pamięci.
whatsisname
5
@whatsisname: To, że nie ulegają awariom, nie oznacza, że ​​nie dyskretnie psują dane. malloc()to jedna z funkcji, na której zdecydowanie chcesz sprawdzić zwracane wartości!
TMN
1
@TMN: Jeśli malloc się nie powiedzie, program natychmiast ulegnie awarii i zakończy działanie, nie będzie kontynuował działania. Chodzi o ryzyko i to, co właściwe, a nie „prawie nigdy”.
whatsisname
2
@Alex C nie brakuje dobrej obsługi błędów. Jeśli chcesz wyjątków, możesz z nich skorzystać. Standardowa biblioteka, która nie korzysta z wyjątków, jest celowym wyborem (wyjątki wymuszają automatyczne zarządzanie pamięcią i często nie jest to konieczne w przypadku kodu niskiego poziomu).
martinkunev,
9

Pytanie nie jest w rzeczywistości specyficzne dla języka, ale raczej dla użytkownika. Pomyśl o tym z perspektywy użytkownika. Użytkownik robi coś, na przykład wpisując nazwę programu w wierszu polecenia i naciskając klawisz Enter. Czego oczekuje użytkownik? Jak mogą stwierdzić, czy coś poszło nie tak? Czy stać ich na interweniowanie w przypadku wystąpienia błędu?

W wielu typach kodu takie kontrole są nadmierne. Jednak w przypadku kodu o wysokiej niezawodności o wysokiej niezawodności, takiego jak w przypadku reaktorów jądrowych, patologiczne sprawdzanie błędów i planowane ścieżki odzyskiwania są częścią codziennego charakteru zadania. Warto poświęcić czas na pytanie „Co się stanie, jeśli X się nie powiedzie? Jak wrócić do bezpiecznego stanu?” W mniej niezawodnym kodzie, takim jak w przypadku gier wideo, można uniknąć znacznie mniejszej kontroli błędów.

Innym podobnym podejściem jest to, jak bardzo możesz poprawić stan, wychwytując błąd? Nie mogę policzyć liczby programów w C ++, które z dumą wychwytują wyjątki, tylko po to, aby je ponownie rzucić, ponieważ tak naprawdę nie wiedziały, co z nimi zrobić ... ale wiedziały, że powinny obsługiwać wyjątki. Takie programy nic nie zyskały na dodatkowym wysiłku. Dodaj tylko kod sprawdzający błędy, który Twoim zdaniem może lepiej poradzić sobie z sytuacją, niż po prostu nie sprawdzając kodu błędu. Biorąc to pod uwagę, C ++ ma określone zasady obsługi wyjątków, które występują podczas obsługi wyjątków, aby złapać je w bardziej znaczący sposób (a przez to mam na myśli wywołanie terminate (), aby upewnić się, że stos pogrzebowy, który zbudowałeś dla siebie, zapali się w odpowiedniej chwale)

Jak patologicznie możesz się dostać? Pracowałem nad programem, który zdefiniował „SafetyBool”, którego prawdziwe i fałszywe wartości zostały starannie wybrane, aby mieć równomierny rozkład 1 i 0, i zostały one wybrane tak, aby jedna z wielu awarii sprzętowych (pojedyncze bity flip, szyna danych ślady ulegają uszkodzeniu itp.) nie spowodowały błędnej interpretacji wartości logicznej. Nie trzeba dodawać, że nie twierdziłbym, że jest to praktyka programowania ogólnego przeznaczenia, którą można zastosować w każdym starym programie.

Cort Ammon
źródło
6
  • Różne wymagania bezpieczeństwa wymagają różnych poziomów poprawności. W oprogramowaniu lotniczym lub sterującym samochodami sprawdzane będą wszystkie zwracane wartości, por. MISRA.FUNC.UNUSEDRET . W krótkim dowodzie koncepcji, który nigdy nie opuszcza twojej maszyny, prawdopodobnie nie.
  • Kodowanie kosztuje czas. Zbyt wiele nieistotnych kontroli w oprogramowaniu niekrytycznym dla bezpieczeństwa jest trudniejsze niż gdzie indziej. Ale gdzie jest słaby punkt między jakością a kosztem? Zależy to od narzędzi do debugowania i złożoności oprogramowania.
  • Obsługa błędów może zaciemniać przepływ sterowania i wprowadzać nowe błędy. Bardzo lubię funkcje otoki Stevensa „sieciowe” Richarda, które przynajmniej zgłaszają błędy.
  • Obsługa błędów rzadko może stanowić problem z wydajnością. Ale większość wywołań biblioteki C potrwa tak długo, że koszt sprawdzenia wartości zwracanej jest niezmiernie mały.
Peter - Przywróć Monikę
źródło
3

Trochę abstrakcyjnego podejścia do pytania. I niekoniecznie jest to język C.

W przypadku większych programów miałbyś warstwę abstrakcji; być może część silnika, biblioteki lub w ramach. Że warstwa nie dbają o pogodzie można uzyskać poprawne dane lub wyjście byłoby jakąś wartość domyślna: 0, -1, nullitd.

Jest też warstwa, która byłaby twoim interfejsem do warstwy abstrakcyjnej, która wymagałaby dużo obsługi błędów i być może innych rzeczy, takich jak wstrzykiwanie zależności, nasłuchiwanie zdarzeń itp.

A później będziesz miał swoją konkretną warstwę implementacyjną, w której faktycznie ustawisz reguły i obsłużysz wyniki.

Uważam więc, że czasem lepiej jest całkowicie wykluczyć obsługę błędów z części kodu, ponieważ ta część po prostu nie wykonuje tego zadania. A potem mieć procesor, który ocenia dane wyjściowe i wskazuje błąd.

Odbywa się to głównie w celu rozdzielenia obowiązków, co prowadzi do czytelności kodu i lepszej skalowalności.

Kreatywna magia
źródło
0

Ogólnie rzecz biorąc, chyba że masz dobry powód, aby nie sprawdzać tego błędu. Jedynym dobrym powodem, dla którego mogę wymyślić, że nie sprawdzam warunku błędu, jest to, że nie możesz zrobić czegoś sensownego, jeśli się nie powiedzie. Jest to jednak bardzo trudny do spełnienia bar, ponieważ zawsze istnieje jedna realna opcja: wyjście z gracją.

Sprytny neologizm
źródło
Ogólnie nie zgadzam się z tobą, ponieważ błędy to sytuacje, których nie uwzględniłeś. Trudno wiedzieć, w jaki sposób może pojawić się błąd, jeśli nie wiesz, w jakich warunkach wystąpił błąd. A zatem obsługa błędów przed faktycznym przetwarzaniem danych.
Creative Magic,
Tak jak powiedziałem, jeśli coś zwróci lub zgłasza błąd, nawet jeśli nie wiesz, jak naprawić błąd i kontynuować, zawsze możesz z wdziękiem zawieść, zalogować błąd, powiedzieć użytkownikowi, że nie działał zgodnie z oczekiwaniami itp. Chodzi o to, że błąd nie powinien pozostać niezauważony, niezalogowany, „po cichu zawieść” lub zawiesić aplikację. To właśnie oznacza „z wdziękiem” lub „z wdziękiem wyjść”.
Clever Neologism