Klasyczny sposób programowania to try ... catch
. Kiedy stosować try
bez catch
?
W Pythonie następujące elementy wydają się zgodne z prawem i mogą mieć sens:
try:
#do work
finally:
#do something unconditional
Jednak kod nic nie catch
zrobił. Podobnie można by pomyśleć w Javie:
try {
//for example try to get a database connection
}
finally {
//closeConnection(connection)
}
Wygląda dobrze i nagle nie muszę się martwić typami wyjątków itp. Jeśli to dobra praktyka, to kiedy to dobra praktyka? Alternatywnie, jakie są powody, dla których nie jest to dobra praktyka lub jest niezgodna z prawem? (Nie skompilowałem źródła. Pytam o to, ponieważ może to być błąd składniowy Java. Sprawdziłem, czy Python na pewno się skompiluje.)
Powiązany problem, z którym się zetknąłem: kontynuuję pisanie funkcji / metody, na końcu której musi coś zwrócić. Może jednak znajdować się w miejscu, do którego nie należy dotrzeć i musi być punktem powrotu. Tak więc, nawet jeśli obsłużę powyższe wyjątki, wciąż zwracam NULL
ciąg znaków lub pusty ciąg w pewnym momencie kodu, który nie powinien zostać osiągnięty, często na końcu metody / funkcji. Zawsze udawało mi się zrestrukturyzować kod, aby nie musiał return NULL
, bo to absolutnie wydaje się mniej niż dobrą praktyką.
źródło
try/catch
nie jest „klasycznym sposobem programowania”. Jest to klasyczny sposób programowania w C ++ , ponieważ w C ++ brakuje właściwej konstrukcji try / last, co oznacza, że musisz zaimplementować gwarantowane odwracalne zmiany stanu przy użyciu brzydkich hacków z udziałem RAII. Ale przyzwoite języki OO nie mają tego problemu, ponieważ zapewniają try / wreszcie. Jest używany do zupełnie innych celów niż try / catch.Odpowiedzi:
Zależy to od tego, czy poradzisz sobie z wyjątkami, które mogą zostać podniesione w tym momencie, czy nie.
Jeśli możesz poradzić sobie z wyjątkami lokalnie, powinieneś i lepiej jest obsłużyć błąd jak najbliżej miejsca, w którym został zgłoszony.
Jeśli nie możesz poradzić sobie z nimi lokalnie, to posiadanie
try / finally
bloku jest całkowicie uzasadnione - zakładając, że jest jakiś kod, który musisz wykonać niezależnie od tego, czy metoda się powiodła, czy nie. Na przykład (z komentarza Neila ) otwarcie strumienia, a następnie przekazanie tego strumienia do wewnętrznej metody, która ma zostać załadowana, jest doskonałym przykładem tego, kiedy będziesz potrzebowaćtry { } finally { }
, używając klauzuli last, aby upewnić się, że strumień jest zamknięty niezależnie od sukcesu lub błąd odczytu.Jednak nadal będziesz potrzebować modułu obsługi wyjątków gdzieś w kodzie - chyba że chcesz, aby aplikacja całkowicie uległa awarii. Zależy to od architektury aplikacji dokładnie tam, gdzie jest ten moduł obsługi.
źródło
try { } finally { }
, korzystając z klauzuli last, aby upewnić się, że strumień zostanie ostatecznie zamknięty bez względu na sukces / niepowodzenie.finally
Blok służy do kodu, który zawsze musi uruchomić, czy warunek błędu (wyjątek) doszło, czy nie.Kod w
finally
bloku jest uruchamiany po zakończeniutry
bloku, a jeśli wystąpił wyjątek, po zakończeniu odpowiedniegocatch
bloku. Jest zawsze uruchamiany, nawet jeśli wystąpił nieprzechwycony wyjątek w blokutry
lubcatch
.finally
Blok jest zazwyczaj używany do plików zamykających, połączeń sieciowych itp, które zostały otwarte wtry
bloku. Powodem jest to, że połączenie pliku lub sieci musi zostać zamknięte, niezależnie od tego, czy operacja przy użyciu tego pliku lub połączenia sieciowego zakończyła się powodzeniem, czy też nie.W
finally
bloku należy zadbać o to, aby sam nie zgłosił wyjątku. Na przykład podwójnie sprawdź wszystkie zmienne pod kątemnull
itp.źródło
try-finally
można zastąpićwith
oświadczeniem.try/finally
. C # mausing
, Python mawith
itp.using
itry-finally
, ponieważDispose
metoda nie będzie wywoływana przezusing
blok, jeśli wystąpi wyjątek w konstruktorzeIDisposable
obiektu.try-finally
pozwala na wykonanie kodu, nawet jeśli konstruktor obiektu zgłasza wyjątek.Przykładem, w którym try ... wreszcie bez klauzuli catch jest właściwe (a nawet idiomatyczne ) w Javie, jest użycie blokady w równoległym pakiecie blokad narzędzi.
źródło
l.lock()
próbę?try{ l.lock(); }finally{l.unlock();}
try
w tym fragmencie ma na celu zawinąć dostęp do zasobów, po co zanieczyszczać go czymś niezwiązanym z tyml.lock()
nie powiedzie,finally
blok będzie nadal działał, jeślil.lock()
jest wtry
bloku. Jeśli zrobisz to tak, jak sugeruje komar,finally
blok uruchomi się tylko wtedy, gdy wiemy, że zamek został przejęty.Na poziomie podstawowym
catch
ifinally
rozwiązać dwa problemy związane jednak różne:catch
służy do obsługi problemu zgłoszonego przez wywołany kodfinally
służy do czyszczenia danych / zasobów utworzonych / zmodyfikowanych przez bieżący kod, bez względu na to, czy wystąpił problem, czy nieOba są więc w jakiś sposób powiązane z problemami (wyjątkami), ale to prawie wszystko, co ich łączy.
Ważną różnicą jest to, że
finally
blok musi być w tej samej metodzie, w której utworzono zasoby (aby uniknąć wycieku zasobów) i nie może być umieszczony na innym poziomie w stosie wywołań.Jest
catch
to jednak inna sprawa: właściwe miejsce zależy od tego, gdzie rzeczywiście można obsłużyć wyjątek. Nie ma sensu łapać wyjątku w miejscu, w którym nie można nic z tym zrobić, dlatego czasem lepiej po prostu pozwolić mu upaść.źródło
finally
załączonej instrukcji try (statycznej lub dynamicznej) ... i nadal być w 100% szczelny.@yfeldblum ma poprawną odpowiedź: spróbuj na końcu bez instrukcji catch zwykle należy zastąpić odpowiednią konstrukcją językową.
W C ++ używa RAII i konstruktorów / destruktorów; w Pythonie jest to
with
instrukcja; a w języku C # jest tousing
stwierdzenie.Są one prawie zawsze bardziej eleganckie, ponieważ kod inicjalizacji i finalizacji znajdują się w jednym miejscu (obiekt abstrakcyjny), a nie w dwóch miejscach.
źródło
W wielu językach
finally
po instrukcji return działa także instrukcja. Oznacza to, że możesz zrobić coś takiego:Które zwalniają zasoby bez względu na to, jak metoda została zakończona za pomocą wyjątku lub zwykłej instrukcji return.
To, czy jest to dobre czy złe, jest przedmiotem dyskusji, ale
try {} finally {}
nie zawsze ogranicza się do obsługi wyjątków.źródło
Mógłbym wywołać gniew Pythonistów (nie wiem, ponieważ nie używam dużo Pythona) lub programistów z innych języków z tą odpowiedzią, ale moim zdaniem większość funkcji nie powinna mieć
catch
bloku, najlepiej mówiąc. Aby pokazać dlaczego, pozwólcie, że skontrastuję to z ręcznym rozpowszechnianiem kodu błędu, takiego jaki musiałem zrobić podczas pracy z Turbo C na przełomie lat 80. i 90.Powiedzmy, że mamy funkcję ładowania obrazu lub czegoś podobnego w odpowiedzi na wybór przez użytkownika pliku obrazu do załadowania, a jest to napisane w C i asemblerze:
Pominąłem niektóre funkcje niskiego poziomu, ale widzimy, że zidentyfikowałem różne kategorie funkcji, oznaczone kolorami, w zależności od ich obowiązków związanych z obsługą błędów.
Punkt awarii i powrotu do zdrowia
Teraz nigdy nie było trudno napisać kategorie funkcji, które nazywam „możliwym punktem awarii” (tymi, które
throw
tzn.) Oraz funkcjami „odzyskiwania i raportowania błędów” (tymi, którecatch
tj.).Funkcje te zawsze były trywialne, aby poprawnie pisać, zanim dostępna była obsługa wyjątków, ponieważ funkcja, która może napotkać awarię zewnętrzną, na przykład brak alokacji pamięci, może po prostu zwrócić a
NULL
lub0
lub-1
lub ustawić globalny kod błędu lub coś w tej sprawie. Odzyskiwanie / raportowanie błędów zawsze było łatwe, ponieważ po przejściu przez stos wywołań do punktu, w którym sensowne było odzyskiwanie i zgłaszanie awarii, wystarczy wziąć kod błędu i / lub komunikat i zgłosić go użytkownikowi. I naturalnie funkcja u progu tej hierarchii, która nigdy, nigdy nie może zawieść, bez względu na to, jak zostanie zmieniona w przyszłości (Convert Pixel
), jest bardzo prosta do napisania poprawnie (przynajmniej w odniesieniu do obsługi błędów).Propagacja błędów
Jednak żmudnymi funkcjami podatnymi na błędy ludzkie były propagatory błędów , te, które nie popadły bezpośrednio w awarię, ale wywołały funkcje, które mogłyby zawieść gdzieś głębiej w hierarchii. W tym momencie
Allocate Scanline
być może trzeba będzie obsłużyć awarię,malloc
a następnie zwrócić błąd doConvert Scanlines
, a następnieConvert Scanlines
będzie musiał sprawdzić ten błąd i przekazać go doDecompress Image
, a następnieDecompress Image->Parse Image
, iParse Image->Load Image
, iLoad Image
na polecenie użytkownika końcowego, gdzie błąd jest wreszcie zgłoszonych .To jest miejsce, w którym wielu ludzi popełnia błędy, ponieważ tylko jeden propagator błędów nie może sprawdzić i przekazać błędu, aby cała hierarchia funkcji upadła, jeśli chodzi o prawidłowe zarządzanie błędem.
Ponadto, jeśli kody błędów są zwracane przez funkcje, prawie tracimy zdolność, powiedzmy, 90% naszej bazy kodów, do zwracania interesujących wartości po sukcesie, ponieważ tak wiele funkcji musiałoby zarezerwować swoją wartość zwrotną na zwrócenie kodu błędu na niepowodzenie .
Ograniczanie błędów ludzkich: globalne kody błędów
Jak więc zmniejszyć prawdopodobieństwo błędu ludzkiego? Tutaj mogę nawet wywołać gniew niektórych programistów C, ale moim zdaniem natychmiastową poprawą jest użycie globalne kodów błędów, takich jak OpenGL
glGetError
. To przynajmniej uwalnia funkcje do zwracania znaczących wartości zainteresowania w przypadku sukcesu. Istnieją sposoby, aby uczynić ten wątek bezpiecznym i wydajnym, gdy kod błędu jest zlokalizowany w wątku.Istnieją również przypadki, w których funkcja może napotkać błąd, ale jest względnie nieszkodliwy, aby działał nieco dłużej, zanim powróci przedwcześnie w wyniku wykrycia poprzedniego błędu. Pozwala to na coś takiego bez konieczności sprawdzania błędów w porównaniu z 90% wywołań funkcji wykonanych w każdej pojedynczej funkcji, dzięki czemu może nadal umożliwiać właściwą obsługę błędów, nie będąc tak drobiazgowym.
Ograniczanie błędów ludzkich: obsługa wyjątków
Jednak powyższe rozwiązanie wciąż wymaga tak wielu funkcji do radzenia sobie z aspektem przepływu sterowania ręcznej propagacji błędów, nawet jeśli mogłoby to zmniejszyć liczbę wierszy
if error happened, return error
kodu typu ręcznego . Nie wyeliminuje go całkowicie, ponieważ często musi istnieć co najmniej jedno miejsce sprawdzające błąd i zwracające prawie każdą funkcję propagacji błędu. Dlatego właśnie pojawia się obsługa wyjątków, aby uratować dzień (sorta).Ale wartością obsługi wyjątków jest tutaj uwolnienie potrzeby radzenia sobie z aspektem przepływu sterowania ręcznej propagacji błędów. Oznacza to, że jego wartość jest związana ze zdolnością do unikania konieczności pisania mnóstwa
catch
bloków w całej bazie kodu. Na powyższym diagramie jedynym miejscem, w którym powinien znajdować sięcatch
blok, jest miejsceLoad Image User Command
zgłoszenia błędu. Nic innego nie powinno idealnie mieć docatch
niczego, ponieważ w przeciwnym razie zaczyna być tak nudne i podatne na błędy, jak obsługa kodów błędów.Więc jeśli mnie spytasz, jeśli masz bazę kodów, która naprawdę korzysta z obsługi wyjątków w elegancki sposób, powinna mieć minimalną liczbę
catch
bloków (przez minimum nie mam na myśli zera, ale bardziej podobną do jednego dla każdego unikalnego wysokiego - operacja użytkownika końcowego, która może zakończyć się niepowodzeniem, a być może nawet mniej, jeśli wszystkie operacje użytkownika wysokiej klasy zostaną wywołane przez centralny system poleceń.Oczyszczanie zasobów
Jednak obsługa wyjątków rozwiązuje jedynie potrzebę unikania ręcznego radzenia sobie z aspektami przepływu sterowania propagacją błędów w wyjątkowych ścieżkach, oddzielnych od normalnych przepływów wykonania. Często funkcja, która służy jako propagator błędów, nawet jeśli robi to teraz automatycznie z EH, może nadal zdobywać zasoby, które musi zniszczyć. Na przykład taka funkcja może otworzyć plik tymczasowy, który musi zamknąć przed powrotem z funkcji bez względu na wszystko, lub zablokować muteks, który musi odblokować bez względu na wszystko.
W tym celu mógłbym wywołać gniew wielu programistów z różnych języków, ale myślę, że podejście C ++ do tego jest idealne. Język wprowadza niszczyciele, które są wywoływane w sposób deterministyczny, gdy tylko obiekt wychodzi poza zakres. Z tego powodu kod C ++, który, powiedzmy, blokuje muteks przez obiekt muteksu o zasięgu z destruktorem, nie musi go ręcznie odblokowywać, ponieważ zostanie automatycznie odblokowany, gdy obiekt wyjdzie poza zakres, bez względu na to, co się stanie (nawet jeśli wyjątek jest napotkane). Tak więc naprawdę nie ma potrzeby, aby dobrze napisany kod C ++ miał kiedykolwiek do czynienia z czyszczeniem zasobów lokalnych.
W językach, w których brakuje destruktorów, może być konieczne użycie
finally
bloku do ręcznego wyczyszczenia lokalnych zasobów. To powiedziawszy, w dalszym ciągu bije konieczność zaśmiecania kodu ręczną propagacją błędów, pod warunkiem , że nie musisz robićcatch
wyjątków w całym dziwacznym miejscu.Odwracanie zewnętrznych efektów ubocznych
To najtrudniejszym problemem do rozwiązania koncepcyjne. Jeśli jakakolwiek funkcja, niezależnie od tego, czy jest to propagator błędów, czy punkt awarii, powoduje zewnętrzne skutki uboczne, musi cofnąć lub „cofnąć” te skutki uboczne, aby przywrócić system do stanu, jakby operacja nigdy nie wystąpiła, zamiast „ połowa ważna ”stan, w którym operacja zakończyła się w połowie. Nie znam języków, które znacznie ułatwiają ten problem pojęciowy, oprócz języków, które po prostu zmniejszają potrzebę wywoływania przez większość funkcji zewnętrznych skutków ubocznych, takich jak języki funkcjonalne, które obracają się wokół niezmienności i trwałych struktur danych.
Tu
finally
jest niewątpliwie jednym z najbardziej eleganckich rozwiązań tam problemu w językach krążących wokół zmienność i skutków ubocznych, ponieważ często ten rodzaj logiki jest bardzo specyficzny dla danej funkcji i nie mapa tak dobrze do koncepcji „oczyszczania zasobów „. I zalecamfinally
swobodne stosowanie w tych przypadkach, aby upewnić się, że twoja funkcja odwraca skutki uboczne w językach, które ją obsługują, niezależnie od tego, czy potrzebujeszcatch
bloku (i ponownie, jeśli mnie zapytasz, dobrze napisany kod powinien mieć minimalną liczbęcatch
bloki, a wszystkiecatch
bloki powinny znajdować się w miejscach, w których jest to najbardziej sensowne, jak na schemacie powyżej wLoad Image User Command
).Wymarzony język
Jednak IMO
finally
jest bliska ideału do odwrócenia skutków ubocznych, ale nie do końca. Musimy wprowadzić jednąboolean
zmienną, aby skutecznie cofnąć skutki uboczne w przypadku przedwczesnego wyjścia (z wyjątku rzuconego lub w inny sposób), tak jak:Gdybym kiedykolwiek mógł zaprojektować język, moim wymarzonym sposobem rozwiązania tego problemu byłoby zautomatyzowanie powyższego kodu:
... z destruktorów zautomatyzować oczyszczanie lokalnych zasobów, co czyni go tak tylko trzeba
transaction
,rollback
icatch
(choć może nadal chcę dodaćfinally
, powiedzmy, współpracując z zasobów C, które nie mycia się w górę). Jednakfinally
zeboolean
zmiennej jest najbliższa rzecz do podejmowania to proste, że znalazłem do tej pory brakowało mój wymarzony język. Drugim najprostszym rozwiązaniem, jakie znalazłem w tym zakresie, jest osłona zakresu w takich językach, jak C ++ i D, ale zawsze uważałem, że osłona zakresu jest nieco niewygodna koncepcyjnie, ponieważ zaciera ideę „czyszczenia zasobów” i „odwracania skutków ubocznych”. Moim zdaniem są to bardzo różne pomysły, którymi należy się zająć w inny sposób.Moje małe marzenie o języku obracałoby się również wokół niezmienności i trwałych struktur danych, aby ułatwić, choć nie jest to konieczne, pisanie wydajnych funkcji, które nie muszą głęboko kopiować masywnych struktur danych w całości, nawet jeśli funkcja ta powoduje bez skutków ubocznych.
Wniosek
W każdym razie, pomijając moje wędrówki, myślę, że twój
try/finally
kod do zamykania gniazda jest w porządku i świetny, biorąc pod uwagę, że Python nie ma odpowiednika destruktorów w C ++, i osobiście uważam, że powinieneś używać go swobodnie w miejscach, które wymagają odwrócenia efektów ubocznych i zminimalizować liczbę miejsc, w których musisz,catch
do miejsc, w których jest to najbardziej sensowne.źródło
Łapanie błędów / wyjątków i porządne postępowanie z nimi jest wysoce zalecane, nawet jeśli nie jest to obowiązkowe.
Powodem, dla którego to mówię, jest to, że uważam, że każdy programista powinien znać zachowanie swojej aplikacji i zająć się nią, w przeciwnym razie nie ukończyłby należycie swojej pracy. Nie ma sytuacji, w której blok „spróbuj nareszcie” zastąpi blok „spróbuj nareszcie”.
Dam ci prosty przykład: Załóżmy, że napisałeś kod do przesyłania plików na serwer bez wychwytywania wyjątków. Teraz, jeśli z jakiegoś powodu przesyłanie nie powiedzie się, klient nigdy nie będzie wiedział, co poszło źle. Ale jeśli złapałeś wyjątek, możesz wyświetlić zgrabny komunikat o błędzie wyjaśniający, co poszło nie tak i jak użytkownik może to naprawić.
Złota zasada: zawsze wychwytuj wyjątek, ponieważ zgadywanie wymaga czasu
źródło
catch
klauzula powinna być zawsze obecna, ilekroć istniejetry
. Bardziej doświadczeni współpracownicy, w tym ja, uważają, że to kiepska rada. Twoje rozumowanie jest błędne. Metoda zawierającatry
to nie jedyne możliwe miejsce, w którym można wychwycić wyjątek. Często najprostszym i najlepszym jest zezwolenie na łapanie i zgłaszanie wyjątków od najwyższego poziomu, zamiast kopiowania klauzul catch w całym kodzie.