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ć catchbloku, 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 throwtzn.) Oraz funkcjami „odzyskiwania i raportowania błędów” (tymi, które catchtj.).
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 NULLlub 0lub-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 Scanlinebyć może trzeba będzie obsłużyć awarię, malloca następnie zwrócić błąd do Convert Scanlines, a następnie Convert Scanlinesbę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 errorkodu 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 catchbloków w całej bazie kodu. Na powyższym diagramie jedynym miejscem, w którym powinien znajdować się catchblok, jest miejsce Load Image User Commandzgłoszenia błędu. Nic innego nie powinno idealnie mieć do catchniczego, 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ę catchblokó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 finallybloku 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ć catchwyją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 finallyjest 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 finallyswobodne 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 catchbloku (i ponownie, jeśli mnie zapytasz, dobrze napisany kod powinien mieć minimalną liczbę catchbloki, a wszystkie catchbloki 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 finallyjest bliska ideału do odwrócenia skutków ubocznych, ale nie do końca. Musimy wprowadzić jedną booleanzmienną, 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, rollbacki 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 finallyze booleanzmiennej 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/finallykod 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, catchdo miejsc, w których jest to najbardziej sensowne.