Czy twierdzić, że jest zło? [Zamknięte]


199

Do Gotwórcy języka napisać :

Go nie zapewnia asercji. Są niezaprzeczalnie wygodne, ale z naszego doświadczenia wynika, że ​​programiści używają ich jako narzędzia, aby uniknąć myślenia o właściwej obsłudze błędów i zgłaszaniu błędów. Właściwa obsługa błędów oznacza, że ​​serwery kontynuują działanie po błędach niekrytycznych zamiast awarii. Właściwe raportowanie błędów oznacza, że ​​błędy są bezpośrednie i do rzeczy, oszczędzając programiście interpretacji dużego śladu awarii. Precyzyjne błędy są szczególnie ważne, gdy programista widzący błędy nie zna kodu.

Jaka jest twoja opinia na ten temat?


4
tangens: Go to niezwykle opiniotwórczy język. To niekoniecznie jest zła rzecz. Oznacza to jednak, że powinieneś przyjmować jego opinie z większym ziarnem soli. Oznacza to również, że jeśli się nie zgodzisz, będziesz zgrzytał zębami podczas używania języka. Jako dowód na to, jak Go trzyma się swoich opinii pomimo rzeczywistości, weź pod uwagę magię refleksji, aby ustalić, czy dwie kolekcje są równe.
allyourcode

@allyourcode Jeśli masz na myśli reflect.DeepEqual, na pewno nie potrzebujesz go. Jest to wygodne, ale kosztem wydajności (testy jednostkowe są dobrym przykładem użycia). W przeciwnym razie możesz bez problemu wprowadzić kontrolę równości odpowiednią dla Twojej „kolekcji”.
Igor Dubinskiy

1
Nie, nie o tym mówię. Nie ma czegoś takiego jak slice1 == slice2 bez odbicia. Wszystkie pozostałe języki mają odpowiednik tej super podstawowej operacji. Jedynym powodem, dla którego Go nie jest, jest uprzedzenie.
allyourcode

Możesz porównać dwa plasterki bez odbicia za pomocą forpętli w Go (podobnie jak C). To byłoby naprawdę miło mieć ogólne operacje plaster, chociaż porównanie komplikuje, gdy wskaźniki i kodowanym są zaangażowane.
kbolino

Odpowiedzi:


321

Nie, nie ma w tym nic złego, o assertile używasz go zgodnie z przeznaczeniem.

Oznacza to, że ma służyć do wychwytywania przypadków, które „nie mogą się zdarzyć” podczas debugowania, w przeciwieństwie do normalnej obsługi błędów.

  • Twierdzenie: Awaria samej logiki programu.
  • Obsługa błędów: błędne dane wejściowe lub stan systemu nie są spowodowane błędem w programie.

109

Nie, ani gotonie assertsą złe. Ale oba mogą być niewłaściwie wykorzystywane.

Assert służy do kontroli poczytalności. Rzeczy, które powinny zabić program, jeśli są niepoprawne. Nie do sprawdzania poprawności lub jako zamiennik obsługi błędów.


jak gotomądrze korzystać ?
ar2015

1
@ ar2015 Znajdź jeden z absurdalnie wymyślonych wzorców, których niektórzy zalecają unikać gotoze względów czysto religijnych, a następnie po prostu używaj gotozamiast zaciemniać to, co robisz. Innymi słowy: jeśli możesz udowodnić, że naprawdę potrzebujesz goto, a jedyną alternatywą jest wprowadzenie mnóstwa bezcelowego rusztowania, które ostatecznie robi to samo, ale bez zmiany policji Goto ... to po prostu użyj goto. Oczywiście warunkiem tego jest „jeśli możesz udowodnić, że naprawdę potrzebujesz goto”. Często ludzie nie. To wciąż nie oznacza, że ​​z natury jest to Zła Rzecz.
underscore_d

2
gotojest używany w jądrze Linuksa do czyszczenia kodu
mal 26.04.16

61

Zgodnie z tą logiką punkty przerwania również są złe.

Aserty powinny być używane jako pomoc w debugowaniu i niczym więcej. „Zło” występuje, gdy próbujesz ich użyć zamiast obsługi błędów.

Zapewnienia służą pomocą, programistom, w wykrywaniu i naprawianiu problemów, które nie mogą istnieć, oraz weryfikacji, czy twoje założenia pozostają prawdziwe.

Nie mają one nic wspólnego z obsługą błędów, ale niestety niektórzy programiści nadużywają ich jako takich, a następnie ogłaszają, że są „źli”.


40

Lubię używać aser. Uważam, że jest to bardzo przydatne, gdy buduję aplikacje po raz pierwszy (być może dla nowej domeny). Zamiast bardzo fantazyjnego sprawdzania błędów (które rozważałbym przedwczesną optymalizację) koduję szybko i dodam wiele stwierdzeń. Po tym, jak dowiem się więcej o tym, jak działają, robię przepisywanie i usuwam niektóre z twierdzeń i zmieniam je, aby poprawić obsługę błędów.

Z powodu stwierdzeń spędzam dużo mniej czasu na programowaniu / debugowaniu programów.

Zauważyłem również, że twierdzenia pomagają mi myśleć o wielu rzeczach, które mogłyby uszkodzić moje programy.


31

Jako dodatkową informację go udostępnia wbudowaną funkcję panic. Można tego użyć zamiast assert. Na przykład

if x < 0 {
    panic("x is less than 0");
}

panicwypisze ślad stosu, więc w jakiś sposób ma to cel assert.


30

Powinny być używane do wykrywania błędów w programie. Niezły wkład użytkownika.

Jeżeli są stosowane prawidłowo, są nie złe.


13

To często się pojawia i myślę, że jednym z problemów, który sprawia, że ​​obrona twierdzeń jest myląca, jest to, że często opierają się one na sprawdzaniu argumentów. Rozważ ten inny przykład, kiedy możesz użyć twierdzenia:

build-sorted-list-from-user-input(input)

    throw-exception-if-bad-input(input)

    ...

    //build list using algorithm that you expect to give a sorted list

    ...

    assert(is-sorted(list))

end

Używasz wyjątku dla danych wejściowych, ponieważ spodziewasz się, że czasami otrzymasz złe dane wejściowe. Twierdzisz, że lista jest posortowana, aby pomóc ci znaleźć błąd w algorytmie, którego z definicji nie oczekujesz. Asercja jest tylko w kompilacji debugowania, więc nawet jeśli sprawdzenie jest drogie, nie masz nic przeciwko robieniu tego przy każdym wywołaniu procedury.

Nadal musisz przetestować kod produkcyjny jednostkowo, ale jest to inny i uzupełniający sposób upewnienia się, że kod jest poprawny. Testy jednostkowe upewniają się, że rutyna spełnia wymagania interfejsu, a twierdzenia są bardziej szczegółowym sposobem upewnienia się, że implementacja działa dokładnie tak, jak tego oczekujesz.


8

Twierdzenia nie są złe, ale można je łatwo nadużyć. Zgadzam się ze stwierdzeniem, że „twierdzenia są często wykorzystywane jako narzędzie, które pozwala uniknąć myślenia o właściwej obsłudze błędów i zgłaszaniu błędów”. Widziałem to dość często.

Osobiście lubię korzystać z asercji, ponieważ dokumentują one założenia, które mogłem poczynić podczas pisania mojego kodu. Jeśli te założenia zostaną złamane przy zachowaniu kodu, problem można wykryć podczas testu. Jednak staram się usuwać wszystkie twierdzenia z mojego kodu podczas tworzenia kompilacji produkcyjnej (tj. Przy użyciu #ifdefs). Usuwając twierdzenia z kompilacji produkcyjnej, eliminuję ryzyko, że ktoś nadużyje ich jako kuli.

Istnieje również inny problem z twierdzeniami. Asercje są sprawdzane tylko w czasie wykonywania. Ale często zdarza się, że sprawdzenie, które chcesz wykonać, mogło zostać wykonane w czasie kompilacji. Lepiej jest wykryć problem w czasie kompilacji. Dla programistów C ++ boost zapewnia BOOST_STATIC_ASSERT, który pozwala to zrobić. Dla programistów C ten artykuł ( tekst linku ) opisuje technikę, której można użyć do wykonywania asercji w czasie kompilacji.

Podsumowując, kieruję się następującą zasadą: Nie używaj asercji w kompilacji produkcyjnej i, jeśli to możliwe, używaj tylko asercji dla rzeczy, których nie można zweryfikować w czasie kompilacji (tj. Muszą być sprawdzone w czasie wykonywania).


5

Przyznaję, że użyłem twierdzeń, nie rozważając jednak właściwego zgłaszania błędów. Nie oznacza to jednak, że są one bardzo pomocne, gdy są właściwie używane.

Są one szczególnie przydatne, jeśli chcesz przestrzegać zasady „Crash Early”. Załóżmy na przykład, że wdrażasz mechanizm zliczania referencji. W niektórych lokalizacjach w kodzie wiesz, że przelicznik powinien wynosić zero lub jeden. Przypuśćmy również, że jeśli ponowne zliczanie jest błędne, program nie zawiedzie natychmiast, ale w następnej pętli komunikatów trudno będzie ustalić, dlaczego coś poszło nie tak. Potwierdzenie byłoby pomocne w wykryciu błędu bliższego jego początkowi.


5

Wolę unikać kodu, który robi różne rzeczy podczas debugowania i wydawania.

Jednak przydatne jest włamanie się do debuggera i posiadanie wszystkich informacji o pliku / linii, a także dokładne wyrażenie i dokładną wartość.

Posiadanie twierdzenia, które „ocenia stan tylko podczas debugowania” może być optymalizacją wydajności i jako takie jest przydatne tylko w 0,0001% programów - gdzie ludzie wiedzą, co robią. We wszystkich innych przypadkach jest to szkodliwe, ponieważ wyrażenie może faktycznie zmienić stan programu:

assert(2 == ShroedingersCat.GetNumEars()); sprawi, że program będzie robił różne rzeczy podczas debugowania i wydawania.

Opracowaliśmy zestaw makr asercji, które rzucałyby wyjątek i robiły to zarówno w wersji debugowania, jak i wersji. Na przykład THROW_UNLESS_EQ(a, 20);wyrzuciłby wyjątek z komunikatem what () zawierającym zarówno plik, linię, jak i rzeczywiste wartości a itd. Tylko makro miałoby na to moc. Debuger może być skonfigurowany tak, aby łamał się podczas „rzucania” określonego typu wyjątku.


4
90% procent statystyki użytej w argumentach jest fałszywe.
João Portela

5

Nie lubię twierdzeń intensywnie. Nie posunąłbym się nawet do stwierdzenia, że ​​są źli.

Zasadniczo, assert zrobi to samo co niesprawdzony wyjątek, jedynym wyjątkiem jest to, że assert (normalnie) nie powinien być zachowany dla produktu końcowego.

Jeśli budujesz sieć bezpieczeństwa dla siebie podczas debugowania i budowania systemu, dlaczego odmawiasz tej siatki bezpieczeństwa swojemu klientowi, działowi pomocy technicznej lub komukolwiek, kto będzie mógł korzystać z oprogramowania, które obecnie budujesz. Używaj wyjątków wyłącznie w odniesieniu do stwierdzeń i wyjątkowych sytuacji. Tworząc odpowiednią hierarchię wyjątków, będziesz w stanie bardzo szybko rozróżnić jeden od drugiego. Z wyjątkiem tego czasu twierdzenie pozostaje na miejscu i może dostarczyć cennych informacji w przypadku awarii, która w przeciwnym razie zostałaby utracona.

Tak więc w pełni rozumiem twórców Go, usuwając całkowicie twierdzenia i zmuszając programistów do korzystania z wyjątków do obsługi sytuacji. Istnieje proste wytłumaczenie tego, wyjątek jest po prostu lepszym mechanizmem dla pracy, dlaczego trzymać się archaicznych twierdzeń?


Go nie ma wyjątków. Zwykle powodem używania asercji zamiast wyjątku jest to, że chcesz go pominąć we wdrożeniu ze względu na wydajność. Aserty nie są archaiczne. Przykro mi, że brzmię szorstko, ale ta odpowiedź nie jest pomocna w pierwotnym pytaniu ani poprawna.
Nir Friedman,


3

Niedawno zacząłem dodawać niektóre elementy do mojego kodu i tak to zrobiłem:

Mentalnie dzielę swój kod na kod graniczny i kod wewnętrzny. Kod graniczny to kod, który obsługuje dane wejściowe użytkownika, odczytuje pliki i pobiera dane z sieci. W tym kodzie żądam danych wejściowych w pętli, która wychodzi tylko wtedy, gdy dane wejściowe są prawidłowe (w przypadku interaktywnego wprowadzania danych przez użytkownika), lub zgłaszam wyjątki w przypadku nieodwracalnych uszkodzonych danych pliku / sieci.

Kod wewnętrzny to wszystko inne. Na przykład funkcję, która ustawia zmienną w mojej klasie, można zdefiniować jako

void Class::f (int value) {
    assert (value < end);
    member = value;
}

a funkcja otrzymująca dane wejściowe z sieci może odczytywać jako taka:

void Class::g (InMessage & msg) {
    int const value = msg.read_int();
    if (value >= end)
        throw InvalidServerData();
    f (value);
}

To daje mi dwie warstwy czeków. Wszystko, gdzie dane są określane w czasie wykonywania, zawsze otrzymuje wyjątek lub natychmiastową obsługę błędów. Jednak to dodatkowe sprawdzenie Class::fza pomocą assertinstrukcji oznacza, że ​​jeśli jakiś wewnętrzny kod kiedykolwiek zadzwoni Class::f, nadal będę miał kontrolę poprawności. Mój wewnętrzny kod może nie przekazać poprawnego argumentu (ponieważ mogłem obliczyć valuez pewnej złożonej serii funkcji), więc lubię mieć asercję w funkcji ustawiającej, aby udokumentować, że niezależnie od tego, kto wywołuje funkcję, valuenie może być większa niż lub równy end.

Wydaje się, że pasuje to do tego, co czytam w kilku miejscach, że stwierdzenia powinny być niemożliwe do pogwałcenia w dobrze funkcjonującym programie, podczas gdy wyjątki powinny dotyczyć wyjątkowych i błędnych przypadków, które są nadal możliwe. Ponieważ teoretycznie sprawdzam poprawność wszystkich danych wejściowych, nie powinno być możliwe wyzwolenie mojego twierdzenia. Jeśli tak, mój program jest nieprawidłowy.


2

Jeśli twierdzenia, o których mówisz, oznaczają, że program wymiotuje, a następnie istnieje, twierdzenia mogą być bardzo złe. Nie oznacza to, że zawsze są niewłaściwe w użyciu, są konstrukcją, którą bardzo łatwo można niewłaściwie wykorzystać. Mają też wiele lepszych alternatyw. Takie rzeczy są dobrymi kandydatami do bycia nazywanymi złymi.

Na przykład moduł innej firmy (lub jakikolwiek inny moduł) prawie nigdy nie powinien wychodzić z programu wywołującego. Nie daje to programiście wywołującemu żadnej kontroli nad ryzykiem, jakie program powinien podjąć w tym momencie. W wielu przypadkach dane są tak ważne, że nawet zapisanie uszkodzonych danych jest lepsze niż ich utrata. Aserty mogą zmusić Cię do utraty danych.

Niektóre alternatywy dla stwierdzeń:

  • Za pomocą debugera
  • Konsola / baza danych / inne rejestrowanie
  • Wyjątki
  • Inne rodzaje obsługi błędów

Niektóre referencje:

Nawet osoby, które opowiadają się za twierdzeniem, uważają, że powinny być one wykorzystywane wyłącznie w rozwoju, a nie w produkcji:

Ta osoba twierdzi, że twierdzeń należy używać, gdy moduł ma potencjalnie uszkodzone dane, które nadal występują po zgłoszeniu wyjątku: http://www.advogato.org/article/949.html . Jest to z pewnością uzasadniony punkt, jednak moduł zewnętrzny nigdy nie powinien określać, jak ważne są uszkodzone dane dla programu wywołującego (wychodząc z nich „za”). Właściwym sposobem na poradzenie sobie z tym jest zgłoszenie wyjątku, który wyjaśnia, że ​​program może być teraz w niespójnym stanie. A ponieważ dobre programy składają się głównie z modułów (z małym kodem kleju w głównym pliku wykonywalnym), twierdzenia są prawie zawsze niewłaściwą rzeczą do zrobienia.


1

assert jest bardzo przydatny i pozwala zaoszczędzić wiele czasu na cofanie się, gdy wystąpią nieoczekiwane błędy, zatrzymując program przy pierwszych oznakach problemów.

Z drugiej strony bardzo łatwo jest nadużyć assert.

int quotient(int a, int b){
    assert(b != 0);
    return a / b;
}

Właściwa, poprawna wersja wyglądałaby mniej więcej tak:

bool quotient(int a, int b, int &result){
    if(b == 0)
        return false;

    result = a / b;
    return true;
}

Więc ... na dłuższą metę ... na dużym obrazie ... Muszę się zgodzić, że assertmożna to wykorzystać. Ciągle to robię.


1

assert jest nadużywany do obsługi błędów, ponieważ mniej pisze.

Dlatego jako projektanci języków powinni raczej zauważyć, że poprawną obsługę błędów można wykonać nawet przy mniejszym pisaniu. Wykluczenie asercji, ponieważ mechanizm wyjątków jest pełny, nie jest rozwiązaniem. Och, czekaj, Go też nie ma wyjątków. Szkoda :)


1
Nieźle :-) Wyjątki czy nie, twierdzenia czy nie, fani Go wciąż mówią o tym, jak krótkie są kody.
Moshe Revah,

1

Kiedy to zobaczyłem, miałem ochotę kopnąć autora w głowę.

Cały czas używam twierdzeń w kodzie i ostatecznie je wszystkie zastępuję, gdy piszę więcej kodu. Używam ich, gdy nie napisałem wymaganej logiki i chcę otrzymywać powiadomienia, gdy napotkam kod, zamiast pisać wyjątek, który zostanie usunięty, gdy projekt będzie się kończył.

Wyjątki łatwiej wtapiają się w kod produkcyjny, co mi się nie podoba. Twierdzenie jest łatwiejsze do zauważenia niżthrow new Exception("Some generic msg or 'pretend i am an assert'");


1

Mój problem z tymi odpowiedziami broniącymi twierdzenia nie polega na tym, że nikt wyraźnie nie odróżnia go od zwykłego błędu krytycznego i dlaczego twierdzenie nie może być podzbiorem wyjątku . Co powiesz na to, a jeśli wyjątek nigdy nie zostanie złapany? Czy to sprawia, że ​​jest to stwierdzenie nomenklatury? I dlaczego miałbyś kiedykolwiek chcieć narzucić ograniczenie w języku, w którym można zgłaszać wyjątek, z którym / nic / nie można sobie poradzić?


Jeśli spojrzysz na moją odpowiedź. Moim zastosowaniem jest rozróżnienie „wyjątków” (stwierdzeń), których chcę się pozbyć używanych do debugowania, a wyjątków, które zatrzymuję. Dlaczego miałbym się ich pozbyć? ponieważ bez nich nie byłoby to kompletne. Przykładem jest, jeśli zajmę się 3 przypadkami, a czwarty to todo. Mogę łatwo wyszukać aser, aby znaleźć je w kodzie i poznać jego niekompletność, a następnie użyć wyjątku, który może zostać przypadkowo przechwycony (przez innego programistę) lub trudno stwierdzić, czy jest to wyjątek, czy sprawdzenie logiki, które powinienem rozwiązać w kodzie.

Moim zdaniem jest to kiepski pomysł, na tym samym poziomie co „zapieczętowane” klasy i z tego samego powodu. Zakładasz, że wyjątki, które chcesz zachować, są dopuszczalne w przypadku korzystania z kodu, którego jeszcze nie znasz. Wszystkie wyjątki przechodzą przez te same kanały, a jeśli użytkownik nie chce ich złapać, może tego nie robić. Jeśli to zrobi, on też powinien mieć tę umiejętność. Tak czy inaczej, po prostu przyjmujesz założenia lub odsuwasz swoją praktykę od koncepcji takiej jak twierdzenie.
Evan Carroll

Uznałem, że przykładowe scenariusze są najlepsze. Oto prosty. int func (int i) {if (i> = 0) {console.write ("Liczba jest dodatnia {0}", i); } else {assert (false); // leniwy, aby robić negatywy ATM} return i * 2; <- Jak mam to zrobić bez zapewnień i czy wyjątek jest naprawdę lepszy? i pamiętaj, ten kod zostanie zaimplementowany przed wydaniem.

Pewne wyjątki są lepsze, powiedzmy, że pobieram dane od użytkowników i dzwonię func()z liczbą ujemną. Teraz, nagle z twoimi zapewnieniami, wyciągnąłeś dywan spod mnie i nie dałeś mi szansy na wyzdrowienie, zamiast grzecznie powiedzieć mi, czego żądam, nie można zrobić. Nie ma nic złego w oskarżaniu programu o niewłaściwe zachowanie i cytowaniu go: problem polega na tym, że zacierasz akt egzekwowania prawa i skazujesz przestępcę.
Evan Carroll

W tym sensie POWINIENEŚ powiedzieć, że użytkownik nie może tego zrobić. Aplikacja powinna zakończyć się z komunikatem o błędzie msg, a ktokolwiek debuguje, pamięta coś, co POWINIEN BYĆ WYKONANE, ale nie jest. NIE chcesz, aby to było obsługiwane. Chcesz rozwiązania i przypomnienia, że ​​nie załatwiłeś tej sprawy. i bardzo łatwo jest zobaczyć, jakie przypadki zostały, ponieważ wszystko, co musisz zrobić, to wyszukać aser w kodzie. Obsługa błędu byłaby niewłaściwa, ponieważ kod powinien być w stanie to zrobić, gdy będzie gotowy do produkcji. Pozwalanie programistom na złapanie go i zrobienie czegoś innego jest złe.

1

Tak, twierdzenia są złe.

Często przyzwyczajają się w miejscach, w których należy zastosować właściwą obsługę błędów. Przyzwyczaj się do pisania poprawnej obsługi błędów jakości produkcji od samego początku!

Zwykle przeszkadzają w pisaniu testów jednostkowych (chyba że napiszesz niestandardowe potwierdzenie, które współdziała z twoją wiązką testową). Dzieje się tak często, ponieważ są one używane tam, gdzie należy zastosować odpowiednią obsługę błędów.

Przeważnie są one kompilowane z kompilacji wersji, co oznacza, że ​​żadne z ich „testowania” nie jest dostępne, gdy uruchomisz kod, który faktycznie wypuszczasz; biorąc pod uwagę, że w sytuacjach wielowątkowych najgorsze problemy często pojawiają się tylko w kodzie wydania, może to być złe.

Czasami są kulą dla zepsutych projektów; tzn. konstrukcja kodu pozwala użytkownikowi wywoływać go w taki sposób, że nie powinien być wywoływany, a twierdzenie „zapobiega” temu. Napraw projekt!

Pisałem o tym więcej na moim blogu w 2005 roku tutaj: http://www.lenholgate.com/blog/2005/09/assert-is-evil.html


0

Nie tyle zła, co generalnie przeciwne do zamierzonego. Istnieje oddzielenie między stałym sprawdzaniem błędów a debugowaniem. Assert sprawia, że ​​ludzie myślą, że wszystkie debugowanie powinno być trwałe i powoduje ogromne problemy z czytelnością, gdy jest często używany. Obsługa błędów stałych powinna być lepsza niż tam, gdzie jest to konieczne, a ponieważ asert powoduje własne błędy, jest to dość wątpliwa praktyka.


5
aser jest dobry do deklarowania warunków wstępnych na górze funkcji, i jeśli jest wyraźnie napisany, działa jako część dokumentacji funkcji.
Peter Cordes,

0

nigdy nie używam assert (), przykłady zwykle pokazują coś takiego:

int* ptr = new int[10];
assert(ptr);

To źle, nigdy tego nie robię, co jeśli moja gra przydziela mnóstwo potworów? dlaczego powinienem zawiesić grę, zamiast tego powinieneś obsługiwać błędy z wdziękiem, więc zrób coś takiego:

CMonster* ptrMonsters = new CMonster[10];
if(ptrMonsters == NULL) // or u could just write if(!ptrMonsters)
{
    // we failed allocating monsters. log the error e.g. "Failed spawning 10 monsters".
}
else
{
    // initialize monsters.
}

14
newnigdy nie wraca nullptr, rzuca.
David Stone,

Zauważ, że możesz do tego użyć std :: nothrow .
markand
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.