Debug.Assert vs Exception Throwing

94

Przeczytałem wiele artykułów (i kilka innych podobnych pytań, które zostały opublikowane w StackOverflow) o tym, jak i kiedy używać asercji i dobrze je zrozumiałem. Ale nadal nie rozumiem, jakiego rodzaju motywacja powinna mnie skłonić do korzystania, Debug.Assertzamiast rzucać zwykły wyjątek. Chodzi mi o to, że w .NET domyślną odpowiedzią na niepowodzenie asercji jest „zatrzymanie świata” i wyświetlenie okna komunikatu użytkownikowi. Chociaż tego rodzaju zachowanie można zmodyfikować, uważam to za bardzo irytujące i zbędne, podczas gdy zamiast tego mógłbym po prostu zgłosić odpowiedni wyjątek. W ten sposób mógłbym łatwo zapisać błąd w dzienniku aplikacji tuż przed rzuceniem wyjątku, a poza tym moja aplikacja niekoniecznie się zawiesza.

Dlaczego więc, jeśli w ogóle, miałbym używać Debug.Assertzamiast zwykłego wyjątku? Umieszczenie asercji tam, gdzie nie powinno, mogłoby po prostu spowodować wszelkiego rodzaju „niepożądane zachowanie”, więc z mojego punktu widzenia tak naprawdę nic nie zyskuję, używając asercji zamiast rzucać wyjątek. Zgadzasz się ze mną, czy coś mi tu brakuje?

Uwaga: w pełni rozumiem, jaka jest różnica „w teorii” (debugowanie a wydanie, wzorce użycia itp.), Ale jak widzę, lepiej byłoby rzucić wyjątek zamiast wykonywać asercję. Ponieważ w przypadku wykrycia błędu w wydaniu produkcyjnym nadal chciałbym, aby „asercja” zawiodła (w końcu „narzut” jest absurdalnie mały), więc lepiej będzie, jeśli zamiast tego rzucę wyjątek.


Edycja: Tak jak ja to widzę, jeśli asercja się nie powiodła, oznacza to, że aplikacja weszła w jakiś zepsuty, nieoczekiwany stan. Więc dlaczego miałbym chcieć kontynuować wykonanie? Nie ma znaczenia, czy aplikacja działa w wersji do debugowania czy wydania. To samo dotyczy obu

Społeczność
źródło
1
Jeśli chodzi o rzeczy, o których mówisz, „jeśli błąd zostanie wykryty w wydaniu produkcyjnym, nadal chciałbym, aby„ asercja ”zawiodła”. Należy używać wyjątków
Tom Neyland
1
Wydajność to jedyny powód. Zerowe sprawdzanie wszystkiego przez cały czas może zmniejszyć prędkość, choć może to być całkowicie niezauważalne. Dzieje się tak głównie w przypadkach, które nigdy nie powinny się zdarzyć, np. Wiesz, że zaznaczyłeś ją już w poprzedniej funkcji, nie ma sensu tracić cykli na ponowne sprawdzanie. Debug.assert skutecznie działa jak test jednostkowy ostatniej szansy, aby Cię o tym poinformować.
rolki

Odpowiedzi:

177

Chociaż zgadzam się, że twoje rozumowanie jest wiarygodne - to znaczy, jeśli twierdzenie zostanie naruszone nieoczekiwanie, sensowne jest wstrzymanie wykonania przez rzucanie - osobiście nie użyłbym wyjątków zamiast twierdzeń. Dlatego:

Jak powiedzieli inni, twierdzenia powinny dokumentować sytuacje, które są niemożliwe , w taki sposób, aby w przypadku zaistnienia rzekomo niemożliwej sytuacji informowany był o tym deweloper. Wyjątki natomiast zapewniają mechanizm kontroli przepływu w sytuacjach wyjątkowych, mało prawdopodobnych lub błędnych, ale nie w sytuacjach niemożliwych. Dla mnie kluczowa różnica jest taka:

  • ZAWSZE powinno być możliwe utworzenie przypadku testowego, który ćwiczy daną instrukcję throw. Jeśli nie jest możliwe stworzenie takiego przypadku testowego, oznacza to, że w programie znajduje się ścieżka kodu, która nigdy się nie wykonuje i powinna zostać usunięta jako martwy kod.

  • NIGDY nie powinno być możliwe stworzenie przypadku testowego, który spowoduje pożar asercji. Jeśli asercja zostanie odpalona, ​​albo kod jest błędny, albo potwierdzenie jest błędne; tak czy inaczej, coś musi się zmienić w kodzie.

Dlatego nie zamieniłbym stwierdzenia na wyjątek. Jeśli potwierdzenie nie może faktycznie zostać uruchomione, zastąpienie go wyjątkiem oznacza, że ​​w programie znajduje się niemożliwa do przetestowania ścieżka kodu . Nie lubię niesprawdzalnych ścieżek kodu.

Eric Lippert
źródło
16
Problem z asercjami polega na tym, że nie ma ich w kompilacji produkcyjnej. Niespełnienie zakładanego warunku oznacza, że ​​program wszedł w obszar niezdefiniowanego zachowania, w którym to przypadku odpowiedzialny program musi jak najszybciej zatrzymać wykonywanie (rozwijanie stosu jest również nieco niebezpieczne, w zależności od tego, jak rygorystyczny chcesz uzyskać). Tak, asercje zwykle nie powinny być odpalane , ale nie wiesz, co jest możliwe, gdy coś dzieje się na wolności. To, co uważasz za niemożliwe, może się wydarzyć w produkcji, a odpowiedzialny program powinien wykryć naruszone założenia i działać szybko.
kizzx2,
2
@ kizzx2: OK, więc ile niemożliwych wyjątków na linię kodu produkcyjnego piszesz?
Eric Lippert,
4
@ kixxx2: To jest C #, dzięki czemu można zachować twierdzeń w kodzie produkcyjnym za pomocą Trace.Assert. Możesz nawet użyć pliku app.config, aby przekierować potwierdzenia produkcyjne do pliku tekstowego, zamiast być niegrzecznym dla użytkownika końcowego.
HTTP 410
3
@AnorZaken: Twój punkt widzenia ilustruje błąd projektowy w wyjątkach. Jak zauważyłem w innym miejscu, wyjątkami są (1) śmiertelne katastrofy, (2) poważne błędy, które nigdy nie powinny się zdarzyć, (3) awarie projektowe, w których wyjątek jest używany do zasygnalizowania nietypowego stanu lub (4) nieoczekiwane warunki egzogeniczne . Dlaczego te cztery zupełnie różne rzeczy są reprezentowane przez wyjątki? Jeśli miałem druthers, boneheaded „null został dereferencjonowane” wyjątki nie byłoby połów w ogóle . To nigdy nie jest w porządku i powinno zakończyć działanie programu, zanim zrobi więcej szkody . Powinni bardziej przypominać twierdzenia.
Eric Lippert
2
@Backwards_Dave: fałszywe twierdzenia są złe, a nie prawdziwe. Asercje umożliwiają przeprowadzanie kosztownych kontroli weryfikacyjnych, których nie chcesz uruchamiać w środowisku produkcyjnym. A jeśli asercja zostanie naruszona na produkcji, co powinien z tym zrobić użytkownik końcowy?
Eric Lippert
17

Asercje służą do sprawdzania zrozumienia świata przez programistę. Asercja powinna zawieść tylko wtedy, gdy programista zrobił coś złego. Na przykład nigdy nie używaj potwierdzenia do sprawdzania danych wejściowych użytkownika.

Test potwierdzeń dla warunków, które „nie mogą wystąpić”. Wyjątki dotyczą warunków, które „nie powinny się zdarzyć, ale mają”.

Asercje są przydatne, ponieważ w czasie kompilacji (lub nawet w czasie wykonywania) można zmienić ich zachowanie. Na przykład często w kompilacjach wydania potwierdzenia nie są nawet sprawdzane, ponieważ wprowadzają niepotrzebne obciążenie. Jest to również coś, na co należy uważać: Twoje testy mogą nawet nie zostać wykonane.

Jeśli zamiast potwierdzeń użyjesz wyjątków, stracisz pewną wartość:

  1. Kod jest bardziej rozwlekły, ponieważ testowanie i zgłaszanie wyjątku to co najmniej dwa wiersze, podczas gdy potwierdzenie to tylko jeden.

  2. Twój kod testowy i rzutowy będzie zawsze działał, podczas gdy potwierdzenia można skompilować.

  3. Tracisz komunikację z innymi programistami, ponieważ potwierdzenia mają inne znaczenie niż kod produktu, który sprawdza i generuje. Jeśli naprawdę testujesz asercję programistyczną, użyj asercji.

Więcej tutaj: http://nedbatchelder.com/text/assert.html

Ned Batchelder
źródło
Jeśli to „nie może się zdarzyć”, to po co pisać twierdzenie. Czy to nie jest zbędne? Jeśli rzeczywiście może się to zdarzyć, ale nie powinno, to czy nie jest to to samo, co „nie powinno się zdarzyć, ale zrób”, które jest przeznaczone na wyjątki?
David Klempfner
2
„Nie może się zdarzyć” jest w cudzysłowie nie bez powodu: może się zdarzyć tylko wtedy, gdy programista zrobił coś złego w innej części programu. Assert jest sprawdzianem błędów programisty.
Ned Batchelder
@NedBatchelder Termin programista jest jednak trochę niejednoznaczny, gdy tworzysz bibliotekę. Czy to prawda, że ​​te „niemożliwe sytuacje” powinny być niemożliwe dla użytkownika biblioteki, ale mogłyby być możliwe, gdy autor biblioteki popełnił błąd?
Bruno Zell
12

EDYCJA: W odpowiedzi na edycję / notatkę, którą zrobiłeś w swoim poście: Wygląda na to, że używanie wyjątków jest właściwą rzeczą do używania zamiast używania asercji dla rodzaju rzeczy, które próbujesz osiągnąć. Myślę, że mentalną przeszkodą, którą uderzasz, jest to, że rozważasz wyjątki i stwierdzenia, aby spełnić ten sam cel, a więc próbujesz dowiedzieć się, który z nich byłby „właściwy” w użyciu. Chociaż mogą występować pewne nakładania się w sposobie używania asercji i wyjątków, nie należy mylić tego, że są to różne rozwiązania tego samego problemu - tak nie jest. Każde stwierdzenie i wyjątki ma swój własny cel, mocne i słabe strony.

Miałem zamiar wpisać odpowiedź własnymi słowami, ale ta koncepcja jest bardziej sprawiedliwa niż ja:

Stacja C #: asercje

Użycie instrukcji assert może być skutecznym sposobem wyłapywania błędów logiki programu w czasie wykonywania, a mimo to można je łatwo odfiltrować z kodu produkcyjnego. Gdy programowanie zostanie zakończone, koszty wykonywania tych nadmiarowych testów pod kątem błędów kodowania można wyeliminować po prostu przez zdefiniowanie symbolu preprocesora NDEBUG [który wyłącza wszystkie asercje] podczas kompilacji. Pamiętaj jednak, że kod umieszczony w samym assercie zostanie pominięty w wersji produkcyjnej.

Asercji najlepiej używać do testowania warunku tylko wtedy, gdy wszystkie następujące blokady:

* the condition should never be false if the code is correct,
* the condition is not so trivial so as to obviously be always true, and
* the condition is in some sense internal to a body of software.

Asercje prawie nigdy nie powinny być używane do wykrywania sytuacji, które pojawiają się podczas normalnego działania oprogramowania. Na przykład zwykle potwierdzeń nie należy używać do sprawdzania błędów w danych wejściowych użytkownika. Może jednak mieć sens użycie potwierdzeń, aby sprawdzić, czy wywołujący sprawdził już dane wejściowe użytkownika.

Zasadniczo używaj wyjątków dla rzeczy, które muszą być przechwytywane / obsługiwane w aplikacji produkcyjnej, używaj asercji do wykonywania logicznych kontroli, które będą przydatne podczas programowania, ale zostaną wyłączone w produkcji.

Tom Neyland
źródło
Zdaję sobie z tego sprawę. Ale chodzi o to, że to samo stwierdzenie, które oznaczyłeś jako pogrubione, również odnosi się do wyjątków. Więc tak jak ja to widzę, zamiast asercji mógłbym po prostu rzucić wyjątek (ponieważ jeśli "sytuacja, która nigdy nie powinna wystąpić" wystąpi na wdrożonej wersji, nadal chciałbym być o tym poinformowany [plus, aplikacja może wejść w stan zepsuty, więc ja wyjątek jest odpowiedni, mogę nie chcieć kontynuować normalnego przepływu wykonywania)
1
Asercje powinny być używane na niezmiennikach; wyjątki powinny być używane, gdy, powiedzmy, coś nie powinno mieć wartości null, ale będzie (jak parametr metody).
Ed S.
Myślę, że wszystko sprowadza się do tego, jak defensywnie chcesz kodować.
Ned Batchelder
Zgadzam się, ponieważ wydaje się, że potrzebujesz, wyjątki są drogą do zrobienia. Powiedziałeś, że chcesz: Awarie wykryte w środowisku produkcyjnym, możliwość rejestrowania informacji o błędach, kontrola przepływu wykonania itp. Te trzy rzeczy sprawiają, że myślę, że to, co musisz zrobić, to pominąć kilka wyjątków.
Tom Neyland
7

Myślę, że (wymyślony) praktyczny przykład może pomóc wyjaśnić różnicę:

(zaadaptowane z rozszerzenia Batch MoreLinq )

// 'public facing' method
public int DoSomething(List<string> stuff, object doohickey, int limit) {

    // validate user input and report problems externally with exceptions

    if(stuff == null) throw new ArgumentNullException("stuff");
    if(doohickey == null) throw new ArgumentNullException("doohickey");
    if(limit <= 0) throw new ArgumentOutOfRangeException("limit", limit, "Should be > 0");

    return DoSomethingImpl(stuff, doohickey, limit);
}

// 'developer only' method
private static int DoSomethingImpl(List<string> stuff, object doohickey, int limit) {

    // validate input that should only come from other programming methods
    // which we have control over (e.g. we already validated user input in
    // the calling method above), so anything using this method shouldn't
    // need to report problems externally, and compilation mode can remove
    // this "unnecessary" check from production

    Debug.Assert(stuff != null);
    Debug.Assert(doohickey != null);
    Debug.Assert(limit > 0);

    /* now do the actual work... */
}

Tak więc, jak powiedzieli Eric Lippert i inni, potwierdzasz tylko rzeczy, których oczekujesz, że będą poprawne, na wypadek, gdybyś (programista) przypadkowo użył go niewłaściwie gdzie indziej, abyś mógł naprawić swój kod. Zasadniczo rzucasz wyjątki, gdy nie masz kontroli lub nie możesz przewidzieć tego, co się pojawi, np. W przypadku danych wejściowych użytkownika , aby cokolwiek przekazało złe dane, mogło odpowiednio zareagować (np. Użytkownik).

drzaus
źródło
Czy Twoje 3 potwierdzenia nie są całkowicie zbędne? Niemożliwe jest, aby ich parametry były fałszywe.
David Klempfner
1
O to właśnie chodzi - twierdzenia są po to, aby udokumentować rzeczy, które są niemożliwe. Dlaczego chcesz to zrobić? Ponieważ możesz mieć coś takiego jak ReSharper, które ostrzega Cię wewnątrz metody DoSomethingImpl, że „możesz tutaj wyłuskiwać null” i chcesz mu powiedzieć „Wiem, co robię, to nigdy nie może być zerowe”. Jest to również wskazówka dla jakiegoś późniejszego programisty, który może nie od razu zdać sobie sprawę z połączenia między DoSomething i DoSomethingImpl, zwłaszcza jeśli są one oddalone od siebie o setki wierszy.
Marcel Popescu
4

Kolejny samorodek z Code Complete :

„Asercja to funkcja lub makro, które głośno narzeka, jeśli założenie nie jest prawdziwe. Użyj asercji, aby udokumentować założenia przyjęte w kodzie i usunąć nieoczekiwane warunki. ...

„Podczas opracowywania, asercje usuwają sprzeczne założenia, nieoczekiwane warunki, złe wartości przekazywane rutynom i tak dalej”.

Następnie dodaje kilka wskazówek dotyczących tego, czego należy, a czego nie należy się domagać.

Z drugiej strony wyjątki:

„Użyj obsługi wyjątków, aby zwrócić uwagę na nieoczekiwane przypadki. Wyjątkowe przypadki powinny być obsługiwane w sposób, który sprawia, że ​​są one oczywiste podczas programowania i możliwe do odtworzenia podczas działania kodu produkcyjnego”.

Jeśli nie masz tej książki, powinieneś ją kupić.

Andrew Cowenhoven
źródło
2
Przeczytałem książkę, jest doskonała. Jednak… nie odpowiedziałeś na moje pytanie :)
Masz rację, nie odpowiedziałem. Moja odpowiedź brzmi: nie, nie zgadzam się z tobą. Asercje i wyjątki to różne zwierzęta, jak opisano powyżej, i niektóre inne opublikowane odpowiedzi tutaj.
Andrew Cowenhoven
0

Debug.Assert domyślnie działa tylko w kompilacjach debugowania, więc jeśli chcesz wychwycić jakiekolwiek złe nieoczekiwane zachowanie w kompilacjach wydania, musisz użyć wyjątków lub włączyć stałą debugowania we właściwościach projektu (co jest uwzględnione w generalnie nie jest to dobry pomysł).

Mez
źródło
pierwsze zdanie częściowe jest prawdziwe, reszta jest ogólnie złym pomysłem: asercje są założeniami i brakiem walidacji (jak stwierdzono powyżej), włączenie debugowania w wydaniu naprawdę nie jest opcją.
Marc Wittke
0

Użyj asercji do rzeczy, które możliwe, ale nie powinny się wydarzyć (gdyby to było niemożliwe, dlaczego miałbyś wstawiać asercję?).

Czy to nie brzmi jak przypadek do użycia Exception? Dlaczego miałbyś używać asercji zamiast Exception?

Ponieważ powinien istnieć kod, który zostanie wywołany przed Twoim potwierdzeniem, który zatrzymałby parametr potwierdzenia jako fałszywy.

Zwykle nie ma kodu, Exceptionktóry gwarantuje, że nie zostanie on wyrzucony.

Dlaczego to dobrze, że Debug.Assert()jest kompilowany w wersji produkcyjnej? Jeśli chcesz wiedzieć o tym w debugowaniu, czy nie chciałbyś wiedzieć o tym w wersji produkcyjnej?

Chcesz tego tylko podczas programowania, ponieważ gdy znajdziesz Debug.Assert(false)sytuacje, piszesz kod, aby zagwarantować, że Debug.Assert(false)to się nie powtórzy. Po zakończeniu programowania, zakładając, że znalazłeś Debug.Assert(false)sytuacje i naprawiłeś je, Debug.Assert()można je bezpiecznie skompilować, ponieważ są teraz zbędne.

David Klempfner
źródło
0

Załóżmy, że jesteś członkiem dość dużego zespołu i kilka osób pracuje nad tym samym ogólnym kodem, w tym nad nakładaniem się klas. Możesz utworzyć metodę, która jest wywoływana przez kilka innych metod i aby uniknąć rywalizacji o blokady, nie dodajesz do niej oddzielnej blokady, ale raczej „zakładasz”, że została ona wcześniej zablokowana przez metodę wywołującą z określoną blokadą. Na przykład Debug.Assert (RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); Inni programiści mogą przeoczyć komentarz, który mówi, że metoda wywołująca musi używać blokady, ale nie mogą tego zignorować.

daniel
źródło