Wczoraj naciąłem się na nieprzyjemny rodzaj błędu w kodzie. Opierając się na powiedzeniu “Najciemniej pod latarnią”, można by go określić jako nieprzenikniona ciemność tuż pod najjaśniejszą latarnią w całym mieście. Normalnie nikomu nie przyjdzie do głowy, by jej tam szukać. Chodzi bowiem o jakąś “oczywistość”, w którą wątpienie jest nie tyle nierozsądnie, co wręcz nie przychodzi nawet na myśl.
A wszystko dlatego, że lubię prostotę – przynajmniej w takich kwestiach, jak wyniki funkcji informujące o ewentualnych błędach. Prawie zawsze są to u mnie zwykłe bool
e, informujące jedynie o tym, czy dana operacja się powiodła lub nie. To wystarcza, bo do bardziej zaawansowanego debugowania są logi, wyjątki, asercje i inne dobrodziejstwo inwentarza. A same wywołania funkcji możemy sobie bez przeszkód umieszczać w if
ach czy warunkach pętli.
Są jednak biblioteki, które “twierdzą”, że taka konwencja jest niedobra i proponują inne. Pół biedy, gdy chodzi tu o coś w rodzaju typu HRESULT
, do którego w pakiecie dostarczane są makra SUCCEEDED
i FAILED
. Zupełnie nie rozumiem jednak, co jest pociągającego w pisaniu np.:
Usprawiedliwiać się mogę jednak tym, że… no cóż, to przecież Linux ;-) Ale kiedy dostaję do ręki porządną bibliotekę z klasami, wsparciem dla STL-a i CamelCase w nazwach funkcji, to przecież mogę się spodziewać czegoś rozsądniejszego, prawda? Skąd może mi przyjść do głowy, że rezultat zero ma oznaczać powodzenie wykonania funkcji?!
Najwyraźniej jednak powinienem taką możliwość dopuszczać. Okazuje się bowiem, że nawet dobrze zaprojektowane, estetyczne API może mieć takie kwiatki. Przekonałem się o tym, próbując pobrać wartość atrybutu elementu XML, używając biblioteki TinyXML i jej metody o nazwie QueryStringAttribute
. W przypadku powodzenia zwraca ona stałą TIXML_SUCCESS
; szkoda tylko, że jej wartością jest… zero ;P
Więc zaprawdę powiadam wam: nie ufajcie żadnej funkcji, której typem zwracanym nie jest bool
!
Bardzo powszechna konwencja, dla tego sam też korzystam ze zdefiniowanych w danej bibliotece stałych. I czasami sam wykorzystuje tego typu konwencje (np. aby zachować spójność z resztą interfejsów) – co do zwracania wartości logicznych sygnalizujących błąd, są to bardzo specyficzne przypadki i lepiej chyba jednak skorzystać z wyjątków lub zwracania tej nieszczęsnej pełnej zmiennej sygnalizującej błąd.
Co do wartości zero, siłą rzeczy musi oznaczać powodzenie, pomyśl sobie co by było gdyby, wspomniany kod powodzenia był jedynką a kody błędów liczbami ujemnymi? Zapewne dużo cięższy do zdiagnozowania problem, a tak możesz bezkarnie przetestować zanegowaną wartość i wsio. :)
Co do samego TinyXML to podoba mi się ich rozwiązanie, bo robię if(coś tam) throw(); i koniec. :)
Nawiązując jeszcze do socketów i ogólnie całego API Unix/POSIX, to jest ono dużo starsze od nas. I w czasach gdzie nie było zbyt wymyślnych metod sygnalizowania błędów, socket po prostu zachowuje spójność chociażby z read i write, które w jednej zmiennej potrafią zasygnalizować długość wczytanych danych lub właśnie błąd. W dodatku -1 jest bardzo specyficzną wartością. :)
Moim zdaniem takie właśnie multibłędy są przyczyną największego kurestwa i upadku zasad w informatyce. Zamiast pisać programy to się implementuje tylko błędy i nic nie działa.
@Sebas86: Cywilizowany świat używa wyjątków. Część reszty – w postaci gamedevu – używa asercji, mechanizmów logowania, placeholderów modeli i tekstur, itp. – też dobrze. Nawet starocia typu WinAPI używają jakiegoś errno/GetLastError. Jest też wymysł (chyba) Microsoftu w postaci HRESULT, ale on nie jest zły z tego względu, że (1) każda wartość HRESULT jest != 0 (2) jest on konwencją wspartą dokumentacją i makrami. Takie rozwiązania są okej.
Jest też sprawa pt. o jak złożonej funkcji mówimy. Jeśli wykonuje ona naprawdę skomplikowaną operację, która może się nie powieść z wielu przyczyn, to sygnalizowanie ich za pomocą kilku różnych kodów jest akceptowalne. Ale jeśli to jest pierdoła, takie nic jak pobranie wartości atrybutu XML – który jest albo go nie ma i tyle – to bawienie się w kody błędów jest wyłącznie mylące. I powoduje, notabene, błędy.
Co do samego TinyXML to podoba mi się ich rozwiązanie, bo robię if(coś tam) throw(); i koniec. :)
Czyli, w tym przypadku:
if(QueryStringAttribute(Foo, Bar)) { throw Error(); }
Moim zdaniem dobrze napisany kod powinien dać się przeczytać tak jak zwykły tekst. Ja tu widzę: “Jeżeli pobierzemy/zapytamy o atrybut stringa, to trzeba rzucić wyjątek”. Absurd. Nie taka była intencja autora. To też jeden z powodów, dla których nawet gdy funkcja zwraca zero w przypadku powodzenia to osobiście piszę:
if(QueryStringAttribute(Foo, Bar) != 0) { throw Error(); }
Wtedy przynajmniej można to przeczytać i zrozumieć “w jednym obiegu”.
Cywilizowany świat używa wyjątków.
Ja się cieszę, że nie wszędzie i nie zawsze. Pociąłbym się, gdyby każdego cin’a czy cout’a trzeba było “ubierać” w blok obsługi wyjątków.
BTW:
(…) one of the main causes of the fall of the Roman Empire was that, lacking zero, they had no way to indicate successful termination of their C programs.”
;)
Tylko, że mówimy tutaj o bibliotekach ogólnego przeznaczenia, więc problem sygnalizowania błędów jest potrzebny. Idąc za ciosem sockety również mają dokumentacje, również argument za tym, że rozwiązanie w WinAPI jest akceptowalne jest bez sensu, bo różnica tkwi jedynie w tym, że zamiast umownej stałej mamy makro… takie samo makro możemy stworzyć oczywiście dla funkcji POSIX-owych. W przypadku TinyXML stała daje taką samą czytelność kodu, w dodatku argument z przesadzonym sygnalizowaniem błędów też jest nagięty, po prostu ta biblioteka nie korzysta z wyjątków, a może zdarzyć się, że elementu nie ma, nie można zmienić wartości atrybutu na liczbę zmiennoprzecinkową, itd. Poza tym QueryStringAttribute jest tutaj złym przykładem, mogłeś skorzystać np. z Attribute, który po prostu zwraca wskaźnik do c-stringa bądź null tak jakbyś tego oczekiwał – a to, że zwraca taki sam kod błędów jak reszta funkcji query? Chwała za to projektantom biblioteki, dzięki temu wszystkich funkcji używa się tak samo i nie muszę się dziesięć razy zastanawiać jak użyć akurat tej jednej wybranej.
Powyższy przykład powinien wyglądać tak:
if(QueryStringAttribute(Foo, Bar) != TIXML_SUCCESS) { throw Error(); }
Teraz mam nadzieję jest czytelny. :)
Mam nadzieję, że nie odbierasz tego jako atak osobisty, ale według mnie nieco spłyciłeś problem, nie wszystko da się zrobić tak prosto jak się na pierwszy rzut oka wydaje, a przynajmniej nie w czymś co jest uniwersalne.
Co do globalnego kodu błędu (dobre przykłady to także funkcje w bibliotece standardowej – errno, OpenGL oraz OpenAL). Wydaje się to bardzo dobre, ale i to rozwiązanie ma dwie wady:
– nie da się tego łatwo pogodzić (o ile w ogóle da) z aplikacjami wielowątkowymi, ktoś zastosuje w swojej zmyślnej bibliotece takie rozwiązanie i leżymy…
– jeśli bazujemy na kodzie błędu ustawianym przez ostatnią wykonaną operację musimy uważać na to, czy flaga jest poprawnie ustawiana także w przypadku powodzenia operacji (zazwyczaj nie, więc należy ją ręcznie wyczyścić) oraz sprawdzać czy oby na pewno dana funkcja, którą chcemy testować korzysta z tej flagi (tutaj niestety nie pomoże nawet szybkie spojrzenie na definicję funkcji, po prostu musimy zajrzeć do dokumentacji).
Przeciwko kurestwu ludzi cała masa
Jesteś z Warsztatu i wbijasz w to kutasa
To nie ładnie, zerowa klasa
hajs na górze, zasady na dnie
Dosadnie, jesteście zwykłą bandą frajerów
Dokładnie, ta jazda nie wzięła się z niczego
Małolat chcę coś zrobić – kroi swego
Jest tego coraz więcej na gamedevie
Opadają ci ręce
To dlatego że masz zasady i serce
Masz to coś czego ta kurwa nie posiądzie nigdy więcej
Xion, Regedit rozwiązuje ci ręce
Zaciśnij pięści, z kurestwem walcz jak pitbull aż do śmierci
Wiara w twe zasady drogę ci oświeci
Jestem z tobą, jestem z Warsztatem aż po deskę grobową
C++ RLZ, jeśli to do mnie, znaczy się kogoś uraziłem, to przepraszam. :)
@Seba86
nie, mówie ogólnie o tym że się teraz źle programuje, ludzie piszą jak chcą i nic z tego nie wyniika.
Ja kiedy chcę sprawdzić powodzenie jakiejś funkcji z jakiejś biblioteki (a prawie zawsze chcę), to sięgam do dokumentacji tej biblioteki (którą zawsze mam otwartą), żeby zobaczyć, jak ta funkcja sygnalizuje błąd. Chyba, że pamiętam, jak to tam dokładnie było. W każdym razie nigdy nie zakładałbym, że mogę zrobić if(funkcja()) nie będąc pewnym, co ta funkcja zwraca i jaka wartość oznacza powodzeine a jaka błąd.
Faktycznie, tak jest najlepiej. Jak jednak wiadomo, każdy może mieć gorszy dzień :) Mogę się usprawiedliwiać, że rozleniwiły mnie języki typu C# czy Java, gdzie korzystanie z dokumentacji jest niemalże zbędne, jeśli nie robimy nic bardziej skomplikowanego. Po prostu tooltipy + lista elementów klasy wyświetlana po kropce to często zupełnie wystarczające informacje.
Poza tym nie mam w zwyczaju czytania dokumentacji bibliotek dla samego ich poznania; zwykle ograniczam się do dokładnego przestudiowania przykładów i ew. wykonania kilku eksperymentów. Tutaj chyba zabrakło tej drugiej części, ciężko powiedzieć dlaczego ;)
@C++RLZ: Niespecjalnie ogarniam, co masz na myśli (aczkolwiek nie wiem, czy chcę, abyś to rozwijał ;P).
I przez takie niespójności w składni, język C++ traci rynek :p Na przykład w C# lub Javie nie ma takich banalnych problemów.
A wystarczy zrobić, żeby funkcja zwracała jakiegoś enuma… (choć to nie jest może super elastyczne)
Notka w zasadzie jest po prostu o tym, że nie lubisz domyślnej konwersji int -> bool i wynikającej z tego swobody, która pozwala na mylne “oczywistości” w interpretacji api bibliotek.
Sam morał jednak jak najbardziej trafny: jeżeli coś nie zwraca bool’a, to sprawdź co zwraca.
@Ansa
Jakich “zasad w informatyce”?
Żeby nie wiedzieć jak działa funkcja?
“Zamiast pisać programy to się implementuje tylko błędy i nic nie działa.”
Ciężko się odnieść do braku argumentów.
@dynax
Trochę tutaj się mylisz. (c#+.net)/java nie mają niespójności projektowych w swoich standardowych bibliotekach które wystarczają do wielu zastosowań i dlatego właśnie zyskują rynek.
Nie oznacza to jednak, że ludzie pisząc własne biblioteki, nie popełniają błędów projektowych.
Raczej kwestia punktu odniesienia:
– widzisz POSIXOWE if ( (sock = socket(…)) == -1) sam piszesz multibłędy
– widzisz if (opf.ShowDialog() != DialogResult.OK) sam robisz enumy (jeżeli język pozwala oczywiście)
@Fredny
właśnie nie wystarczy.
enumy w c++ i tak są intami, więc nawet jeśli masz:
MyFancyEnum funkcja() { return MyFancyEnum.OK}
to i tak możesz napisać
if (funkcja()) …
nie czytając co zwróci, a kompilator Ci pozwoli.
Jest parę obejść, ale po pierwsze:
– były omawiane np. na pl.comp.lang.c
– strongly typed enumy (albo przynajmniej rozdzielenie typów arytmetycznych i boola) > obejścia. i dlatego c# 4ever!