Kiedy w kodzie zdarza się coś niedobrego, na co nie mamy natychmiastowego rozwiązania, zwykle rzucamy wyjątek. To nam odwija stos, wychodząc po kolei z głęboko zagnieżdżonych funkcji – aż w końcu natrafimy na handler, który potrafi rzeczony błąd obsłużyć. Odwiecznym problemem wyjątków jest to, gdzie należy tak naprawdę je łapać; zwykle bardzo łatwo jest umieścić kod ich obsługi na zbyt wysokim poziomie, tłumacząc się, że przecież “niżej” nic na ten błąd nie można było poradzić.
Faktycznie zaradzić wyjątkowi często nie można, ale prawie zawsze można w tym pomóc już na wczesnym etapie jego obsługi. Błąd gdzieś “w środku” powoduje bowiem niepowodzenie wywołania wszystkich funkcji po drodze, a każda z takich porażek jest specyficzna dla czynności, która dana funkcja wykonuje. Jeśli przykładowo chcemy odczytać jakiś parametr konfiguracyjny programu, który wymaga załadowania z pliku, to może się okazać, że tego pliku nie da się otworzyć. Wówczas odczytywanie konfiguracji nie powiedzie się – co samo w sobie jest błędem, ale także powoduje, że niemożliwe jest uzyskanie wartości żądanego parametru – kolejny błąd. Widać więc, że błędy-wyjątki mogą być dla siebie kolejno przyczyną i skutkiem.
Taka szczegółowa “historia błędów” jest przydatna, bo pozwala oddzielić informacje ważniejsze od mniej ważnych. Nasza aplikacja może na przykład w ogóle nie wiedzieć, że jej konfiguracja jest zapisywana w pliku (a nie np. Rejestrze) i dlatego wyjątek ‘Nie znaleziono pliku konfiguracyjnego’ powinien być przed nią ukryty. Trzeba go opakować w błąd wyższego rzędu.
Nazywa się to wewnętrznymi wyjątkami (inner exceptions) i jako wbudowany mechanizm występuje chociażby w .NET i Javie. Idea jest prosta: kiedy złapiemy wyjątek “z dołu”, którego nie możemy do końca obsłużyć, zapakowujemy go w nowy obiekt, sygnalizujący niepowodzenie na “naszym” poziomie kodu. Ten nowy wyjątek wyrzucamy dalej; może on potem podlegać bardzo podobnemu procesowi.
I tak w przykładzie z ładowaniem konfiguracji, próba odczytania nieistniejącego pliku skończy się w .NET wyjątkiem IOException
, który zostanie pewnie natychmiast opakowany w FileNotFoundException
. Funkcja odczytująca dane konfiguracyjne z pliku najpewniej sama wsadzi ten wyjątek w nowy, np. ArgumentException
, co mówi wywołującemu, że przekazany argument – tu: nazwa pliku z konfiguracją – jest nieprawidłowy. W końcu, ponieważ nie będzie można odczytać żądanej wartości, funkcja która miała ją zwrócić może nas ostatecznie uraczyć wyjątkiem InvalidOperationException
, zawierającym w środku wszystkie wymienione wcześniej błędy. Których było zresztą całkiem sporo :)
Gdy w końcu złapiemy cały ten wielokrotnie zapakowany wyjątek w innym miejscu, możemy dostać się do całego łańcucha przyczynowo-skutkowego, który powstał podczas odwijania stosu. W tym celu korzysta się z właściwości InnerException
w .NET lub metody getCause
w Javie – na przykład tak:
Zapewne jednak rzadko będziemy sięgali aż do samego dna – zwykle tylko w celach logowania. Dzięki odpowiedniemu opakowaniu możemy bowiem zająć się od razu sytuacją wyjątkową “najwyższego rzędu” – adekwatną do czynności, którą chcieliśmy wykonać (a nie tą pochodzącą z głębokich czeluści kodu niższego poziomu, od której wszystko się zaczęło).
Chyba właśnie coś do mnie dotarło.
Dzięki ;)
Wyjątki są jak ogry – mają warstwy i śmierdzą ;)
Ale są też różnice: wyjątki się czasem przydają, ale czasem :P
Ja kiedyś, nie wiedząc o tych zagnieżdżonych wyjątkach, napotkałem ten sam problem w swoim kodzie C++ i wymyśliłem takie rozwiązanie, żeby klasa wyjątku miała stos stringów i każde złapanie wyjątku po drodze po prostu dopisało do tego stosu nowy komunikat. Stosuję go do dziś i działa bardzo dobrze.