Jak zgłaszać błędy w bibliotekach naukowych?


11

W różnych dyscyplinach inżynierii oprogramowania istnieje wiele filozofii dotyczących tego, jak biblioteki powinny radzić sobie z błędami lub innymi wyjątkowymi warunkami. Kilka z tych, które widziałem:

  1. Zwraca kod błędu z wynikiem zwróconym przez argument wskaźnika. To właśnie robi PETSc.
  2. Zwraca błędy według wartości wartownika. Na przykład malloc zwraca NULL, jeśli nie może przydzielić pamięci, sqrtzwróci NaN, jeśli podasz liczbę ujemną itp. Takie podejście jest stosowane w wielu funkcjach libc.
  3. Rzuć wyjątki. Używane w deal.II, Trilinos itp.
  4. Zwraca typ wariantu; na przykład funkcja C ++, która zwraca obiekt typu, Resultjeśli działa poprawnie i używa typu Errordo opisania, w jaki sposób nie powróciłby std::variant<Error, Result>.
  5. Użyj assert i crash. Używany w p4est i niektórych częściach igraph.

Problemy z każdym podejściem:

  1. Sprawdzanie każdego błędu wprowadza wiele dodatkowego kodu. Wartości, w których wynik zostanie zapisany, muszą zawsze zostać zadeklarowane jako pierwsze, wprowadzając wiele zmiennych tymczasowych, których można użyć tylko raz. To podejście wyjaśnia, jaki błąd wystąpił, ale może być trudno ustalić, dlaczego lub, w przypadku głębokiego stosu wywołań, gdzie.
  2. Przypadek błędu można łatwo zignorować. Co więcej, wiele funkcji nie może mieć nawet znaczącej wartości wartownika, jeśli cały zakres typów danych wyjściowych jest wiarygodnym wynikiem. Wiele takich samych problemów jak # 1.
  3. Możliwe tylko w C ++, Python itp., Nie w C ani Fortran. Może być naśladowany w C za pomocą setjmp / longjmp sorcery lub libunwind .
  4. Możliwe tylko w C ++, Rust, OCaml itp., Nie w C ani Fortran. Można naśladować w C za pomocą makro-magii.
  5. Prawdopodobnie najbardziej informacyjny. Ale jeśli zastosujesz to podejście, powiedzmy, dla biblioteki C, dla której następnie napiszesz opakowanie Pythona, głupi błąd, taki jak przekazanie tablicy spoza zakresu do tablicy, spowoduje awarię interpretera Pythona.

Wiele porad w Internecie na temat obsługi błędów napisano z punktu widzenia systemów operacyjnych, programowania wbudowanego lub aplikacji internetowych. Awarie są niedopuszczalne i musisz martwić się o bezpieczeństwo. Aplikacje naukowe nie mają tych problemów w prawie takim samym stopniu, jeśli w ogóle.

Inną kwestią jest to, jakie rodzaje błędów można naprawić, czy nie. Awarii malloc nie można odzyskać, aw każdym razie zabójca braku pamięci w systemie operacyjnym dostanie się do niego, zanim to zrobisz. Indeks poza zakresem dla rozmiaru tablicy również nie jest możliwy do odzyskania. Dla mnie, jako użytkownika, najprzyjemniejszą rzeczą, jaką może zrobić biblioteka, jest awaria z informacyjnym komunikatem o błędzie. Z drugiej strony, niepowodzenie, powiedzmy, iteracyjnego liniowego solwera w zbieżności można odzyskać, stosując bezpośredni solwery faktoryzacji.

W jaki sposób biblioteki naukowe powinny zgłaszać błędy i oczekiwać ich usunięcia? Zdaję sobie oczywiście sprawę, że zależy to od języka, w którym biblioteka jest zaimplementowana. Ale o ile wiem, w przypadku każdej wystarczająco użytecznej biblioteki ludzie będą chcieli nazywać ją z innego języka niż ten, w którym jest zaimplementowana.

Nawiasem mówiąc, myślę, że podejście nr 5 można znacznie ulepszyć dla biblioteki C, jeśli zdefiniuje wskaźnik funkcji globalnego modułu obsługi asercji jako część publicznego API. Moduł obsługi asercji domyślnie raportuje numer pliku / linii i ulega awarii. Powiązania C ++ dla tej biblioteki zdefiniowałyby nowy moduł obsługi asercji, który zamiast tego zgłasza wyjątek C ++. Podobnie powiązania w języku Python definiują moduł obsługi asercji, który korzysta z interfejsu API CPython w celu zgłoszenia wyjątku w języku Python. Ale nie znam żadnych przykładów, które przyjmują takie podejście.


Inną kwestią są konsekwencje wydajności. Jak te różne metody wpływają na szybkość oprogramowania? Czy powinniśmy stosować inną obsługę błędów w „kontrolnych” częściach kodu (np. Przetwarzanie plików wejściowych) w porównaniu z kosztownymi obliczeniowo „silnikami”?
LedHead

Pamiętaj, że najlepsza odpowiedź różni się w zależności od języka.
Chrylis -on strike-

Odpowiedzi:


10

Dam ci moją perspektywę, która jest zakodowana w projekcie deal.II, do którego się odwołujesz.

Po pierwsze, istnieją dwa rodzaje warunków błędów: błędy, które można odzyskać i błędy, których nie można odzyskać.

  • Pierwsze dotyczy na przykład sytuacji, gdy pliku wejściowego nie można odczytać - na przykład gdy czytasz informacje z takiego pliku, $HOME/.dealiiktóry może istnieć lub nie. Funkcja czytania powinna po prostu wrócić do funkcji wywoływania, aby ta ostatnia mogła dowiedzieć się, co zrobić. Może się również zdarzyć, że zasób nie jest w tej chwili dostępny, ale może być ponownie za minutę (zdalnie zamontowany system plików).

  • To drugie, na przykład, jeśli próbujesz dodać wektor o rozmiarze 10 do wektora o rozmiarze 20: Spróbuj, jak możesz, nic nie można na to poradzić - w kodzie jest błąd punkt, w którym próbowaliśmy dodać.

Te dwa warunki należy traktować inaczej, niezależnie od używanego języka programowania:

  • W drugim przypadku, ponieważ nie ma możliwości odwołania, zakończ program. Możesz to zrobić, zgłaszając wyjątek lub zwracając kod błędu wskazujący dzwoniącemu, że nic nie można zrobić, ale równie dobrze możesz przerwać program od razu, ponieważ znacznie ułatwia to programistom debugowanie problemu.

  • W pierwszym przypadku powstała wyjątkowa sytuacja, z którą można sobie poradzić. Mimo że C i Fortran nie mieli możliwości tego wyrazić, wszystkie rozsądne języki, które pojawiły się później, włączyły sposoby do standardu językowego, aby poradzić sobie z takimi „wyjątkowymi” zwrotami, zapewniając, no cóż, „wyjątki”. Użyj ich - po to są; są również zaprojektowane w taki sposób, że nie można ich zapomnieć o ich zignorowaniu (jeśli tak, wyjątek rozprzestrzenia się tylko o jeden poziom wyżej).

Innymi słowy, zalecam tutaj (i to, co robi deal.II) to mieszanka twoich strategii 3 i 5, w zależności od kontekstu. To prawda, że ​​3 nie działa w językach takich jak C lub Fortran - w takim przypadku można argumentować, że jest to dobry powód, aby po prostu nie używać języków, które utrudniają wyrażenie tego, co chcesz zrobić.

x), ale ponieważ należy wielokrotnie wywoływać ewaluatora, nie powinien on po prostu zawieszać się, ale po prostu zgłaszać wyjątek. W takich przypadkach, mimo że przekazanie wartości ujemnej nie jest możliwe do odzyskania, należy raczej rzucić wyjątek niż przerwać program. Kilka lat temu nie zgodziłem się z tym stanowiskiem, ale zmieniłem zdanie po tym, jak wytyczne dla oprogramowania społeczności xSDK zakodowały wymóg, aby programy nigdy nie ulegały awariom (a przynajmniej powinny mieć sposób na przejście z awarii na wyjątek - więc radzę sobie. II ma teraz opcję uczynienia Assertwyjątku zamiast dzwonienia abort().)


Po prostu zaleciłbym coś przeciwnego: rzuć wyjątek, gdy sytuacja nie może być obsłużona, i zwróć kod błędu, gdy można ją obsłużyć. Problem polega na tym, że radzenie sobie z zgłaszanymi wyjątkami jest trudne: programista aplikacji musi znać typ wszystkich możliwych wyjątków, aby je wychwycić i obsłużyć, w przeciwnym razie program po prostu się zawiesi. Awarie są w porządku, a nawet mile widziane w sytuacjach, których nie można sobie poradzić, ponieważ punkt awarii jest zgłaszany po wyjęciu z pudełka z pythonem, np. Ale w sytuacjach, które można poradzić, nie jest (w większości) mile widziane.
cdalitz

@cdalitz: To wada projektowa C ++, że możesz rzucać obiektami dowolnego typu. Ale każde rozsądne oprogramowanie (wyłączając Trilinos) zgłasza jedynie wyjątki, które są pochodnymi std::exceptioni można je wychwycić przez odniesienie bez znajomości typu pochodnego.
Wolfgang Bangerth,

1
Jednak zdecydowanie nie zgadzam się na zwrócenie kodu błędu z powodów przedstawionych w pierwotnym pytaniu: (i) Kody błędów są zbyt często ignorowane, w związku z czym błędy nie są w ogóle obsługiwane; (ii) w wielu przypadkach po prostu nie ma wyjątkowej wartości, którą można rozsądnie zwrócić, biorąc pod uwagę, że typ zwracanej funkcji jest ustalony; (iii) funkcje mają różne typy zwracanych danych i w każdym przypadku trzeba będzie zdefiniować osobno, jaka byłaby „wyjątkowa” wartość reprezentująca błąd.
Wolfgang Bangerth,

WB napisał (przepraszam, sztuczka „@” z jakiegoś powodu nie działa, a nazwa użytkownika jest z jakiegoś powodu usuwana przez StackExchage): „Kody błędów są zbyt często ignorowane”. Dotyczy to jeszcze bardziej wyjątków: niewielu programistów ma problem z brakowaniem każdego wywołania funkcji w bloku try / catch. Ale to głównie kwestia gustu: dopóki dokumentacja jasno określa, czy i jakie wyjątki rzuca funkcja, mogę sobie z tym poradzić. Ale znowu można powiedzieć: obowiązek pisania dokumentacji jest zbyt często ignorowany ;-)
cdalitz

Ale chodzi o to, że jeśli zapomnisz złapać wyjątek, nie ma żadnych dalszych problemów: program po prostu przerywa. Łatwo będzie ustalić, gdzie wystąpił problem. Jeśli zapomnisz sprawdzić kod błędu, Twój program może ulec awarii w późniejszym czasie z powodu nieokreślonego stanu wewnętrznego - ale gdzie pierwotny problem był całkowicie niejasny. Niezwykle trudno jest znaleźć tego rodzaju błędy.
Wolfgang Bangerth,
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.