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óre catch
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
lub 0
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 do Convert Scanlines
, a następnie Convert Scanlines
będzie musiał sprawdzić ten błąd i przekazać go do Decompress Image
, a następnie Decompress 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 miejsce Load Image User Command
zgłoszenia błędu. Nic innego nie powinno idealnie mieć do catch
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 zalecam finally
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 potrzebujesz catch
bloku (i ponownie, jeśli mnie zapytasz, dobrze napisany kod powinien mieć minimalną liczbę catch
bloki, a wszystkie catch
bloki powinny znajdować się w miejscach, w których jest to najbardziej sensowne, jak na schemacie powyżej w Load 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:
bool finished = false;
try
{
// Cause external side effects.
...
// Indicate that all the external side effects were
// made successfully.
finished = true;
}
finally
{
// If the function prematurely exited before finishing
// causing all of its side effects, whether as a result of
// an early 'return' statement or an exception, undo the
// side effects.
if (!finished)
{
// Undo side effects.
...
}
}
Gdybym kiedykolwiek mógł zaprojektować język, moim wymarzonym sposobem rozwiązania tego problemu byłoby zautomatyzowanie powyższego kodu:
transaction
{
// Cause external side effects.
...
}
rollback
{
// This block is only executed if the above 'transaction'
// block didn't reach its end, either as a result of a premature
// 'return' or an exception.
// Undo side effects.
...
}
... z destruktorów zautomatyzować oczyszczanie lokalnych zasobów, co czyni go tak tylko trzeba transaction
, rollback
i catch
(choć może nadal chcę dodać finally
, powiedzmy, współpracując z zasobów C, które nie mycia się w górę). Jednak finally
ze boolean
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.