Niektóre języki (takie jak C ++ i wcześniejsze wersje PHP) nie obsługują finally
części try ... catch ... finally
konstruktu. Czy finally
kiedykolwiek jest to konieczne? Ponieważ kod w nim zawsze działa, dlaczego nie powinienem / nie powinienem po prostu umieszczać tego kodu po try ... catch
bloku bez finally
klauzuli? Dlaczego warto skorzystać (Szukam powodu / motywacji do używania / nieużywania finally
, nie ma powodu, aby pozbyć się „catch” lub dlaczego jest to legalne.)
exception-handling
Agi Hammerthief
źródło
źródło
Odpowiedzi:
Oprócz tego, co powiedzieli inni, możliwe jest również wprowadzenie wyjątku w klauzuli catch. Rozważ to:
W tym przykładzie
Cleanup()
funkcja nigdy nie działa, ponieważ wyjątek zostaje zgłoszony w klauzuli catch i złapie ją następny najwyższy haczyk w stosie wywołań. Użycie bloku w końcu usuwa to ryzyko i sprawia, że kod jest ładniejszy do rozruchu.źródło
Jak wspomnieli inni, nie ma gwarancji, że kod po
try
instrukcji zostanie wykonany, chyba że złapiesz każdy możliwy wyjątek. To powiedziawszy to:można przepisać 1 jako:
Ale ta ostatnia wymaga wychwycenia wszystkich nieobsługiwanych wyjątków, zduplikowania kodu czyszczenia i pamiętania o ponownym rzuceniu. Nie
finally
jest to konieczne , ale jest przydatne .C ++ nie ma,
finally
ponieważ Bjarne Stroustrup uważa, że RAII jest lepszy lub przynajmniej wystarcza w większości przypadków:1 Konkretny kod do przechwytywania wszystkich wyjątków i ponownego zgłaszania bez utraty informacji śledzenia stosu różni się w zależności od języka. Użyłem języka Java, w którym przechwytywany jest ślad stosu po utworzeniu wyjątku. W języku C # wystarczy użyć
throw;
.źródło
handleError()
drugim przypadku, nie?catch (Throwable t) {}
, próbując złapać blok wokół całego początkowego bloku (aby również złapać przedmioty do rzucaniahandleError
)handleErro();
co sprawi, że będzie to jeszcze lepszy argument, dlaczego w końcu bloki są użyteczne (chociaż nie było to pierwotne pytanie).finally
, co jest o wiele bardziej dopracowane.try
jest wewnątrzcatch
dla określonego wyjątku. Po drugie, możliwe, że nie wiesz, czy możesz z powodzeniem poradzić sobie z błędem, dopóki nie przeanalizujesz wyjątku, lub że przyczyna wyjątku również uniemożliwia obsługę błędu (przynajmniej na tym poziomie). Jest to dość powszechne podczas wykonywania operacji we / wy. Ponowne rzutowanie istnieje, ponieważ jedynym sposobem na zagwarantowaniecleanUp
uruchomienia jest wyłapanie wszystkiego , ale oryginalny kod zezwalałby na wyjątki pochodzące zcatch (SpecificException e)
bloku na propagowanie w górę.finally
bloki są zwykle używane do czyszczenia zasobów, co może pomóc w czytelności przy użyciu wielu instrukcji return:vs
źródło
finally
. (Jak najwyraźniej już przypuszczałeś, tak, C ++ zapewnia te same możliwości bez tego mechanizmu. Jako taki, ściśle mówiąc, mechanizm
try
/finally
nie jest tak naprawdę konieczny.To powiedziawszy, bez niego nakłada się pewne wymagania dotyczące sposobu zaprojektowania reszty języka. W C ++ ten sam zestaw akcji jest wbudowany w destruktor klasy. Działa to przede wszystkim (wyłącznie?), Ponieważ wywołanie destruktora w C ++ jest deterministyczne. To z kolei prowadzi do dość skomplikowanych zasad dotyczących czasu życia obiektów, z których niektóre są zdecydowanie nieintuicyjne.
Większość innych języków zapewnia zamiast tego jakąś formę śmiecia. Chociaż istnieją pewne kwestie związane z odśmiecaniem pamięci, które są kontrowersyjne (np. Jego wydajność w porównaniu z innymi metodami zarządzania pamięcią), jedna rzecz na ogół nie jest: dokładny czas, kiedy obiekt zostanie „wyczyszczony” przez moduł odśmiecający, nie jest bezpośrednio związany do zakresu obiektu. Zapobiega to jego wykorzystaniu, gdy czyszczenie musi być deterministyczne, gdy jest po prostu wymagane do prawidłowego działania lub w przypadku zasobów tak cennych, że ich czyszczenie nie może być opóźnione w sposób arbitralny.
try
/finally
zapewnia sposób, w jaki takie języki radzą sobie z sytuacjami, które wymagają tego deterministycznego czyszczenia.Myślę, że ci, którzy twierdzą, że składnia C ++ dla tej możliwości jest „mniej przyjazna” niż Java, raczej nie rozumieją tego. Co gorsza, brakuje im znacznie ważniejszego punktu na temat podziału odpowiedzialności, który wykracza daleko poza składnię i ma znacznie więcej wspólnego ze sposobem projektowania kodu.
W C ++ to deterministyczne czyszczenie odbywa się w destruktorze obiektu. Oznacza to, że obiekt można (i normalnie powinien być) zaprojektowany do czyszczenia po sobie. Odnosi się to do istoty projektowania obiektowego - klasa powinna być zaprojektowana w celu zapewnienia abstrakcji i egzekwowania własnych niezmienników. W C ++ robi się dokładnie to - a jednym z niezmienników, dla których zapewnia, jest to, że gdy obiekt zostanie zniszczony, zasoby kontrolowane przez ten obiekt (wszystkie, nie tylko pamięć) zostaną poprawnie zniszczone.
Java (i podobne) są nieco inne. Chociaż obsługują (w pewnym sensie) obsługę,
finalize
która teoretycznie może zapewnić podobne możliwości, obsługa jest tak słaba, że w zasadzie jest bezużyteczna (i w zasadzie nigdy nie jest używana).W rezultacie, zamiast sama klasa mogła wykonać wymagane czyszczenie, klient klasy musi podjąć odpowiednie kroki. Jeśli zrobimy wystarczająco krótkowzroczne porównanie, na pierwszy rzut oka może się wydawać, że różnica ta jest dość niewielka, a Java pod tym względem dość konkurencyjna względem C ++. W efekcie powstaje coś takiego. W C ++ klasa wygląda mniej więcej tak:
... a kod klienta wygląda mniej więcej tak:
W Javie wymieniamy trochę więcej kodu, w którym obiekt jest używany nieco mniej w klasie. Ten początkowo wygląda całkiem nawet kompromisu. W rzeczywistości, to jest daleko od tego jednak, bo w większości typowych kodu tylko definiujemy klasę w jednym miejscu, ale używać go w wielu miejscach. Podejście C ++ oznacza, że ten kod piszemy tylko w jednym miejscu. Podejście Java oznacza, że musimy napisać ten kod, aby obsłużyć czyszczenie wiele razy, w wielu miejscach - w każdym miejscu, w którym używamy obiektu tej klasy.
Krótko mówiąc, podejście Java zasadniczo gwarantuje, że wiele abstrakcji, które staramy się zapewnić, jest „nieszczelnych” - każda klasa, która wymaga deterministycznego czyszczenia zobowiązuje klienta tej klasy do uzyskania szczegółowych informacji na temat tego, co należy wyczyścić i jak to zrobić , a nie te szczegóły są ukryte w samej klasie.
Chociaż nazwałem to „podejściem Java” powyżej i
try
/finally
lub podobne mechanizmy pod innymi nazwami nie są całkowicie ograniczone do Javy. W jednym widocznym przykładzie większość (wszystkie?) Języków .NET (np. C #) zapewnia to samo.Ostatnie iteracje zarówno Java, jak i C # również pod tym względem stanowią coś w połowie drogi między „klasyczną” Javą a C ++. W języku C # obiekt, który chce zautomatyzować czyszczenie, może zaimplementować
IDisposable
interfejs, który zapewniaDispose
metodę (przynajmniej niejasną) podobną do destruktora C ++. Podczas gdy można tego użyć za pomocątry
/finally
jak w Javie, C # nieco automatyzuje zadanie za pomocąusing
instrukcji, która pozwala zdefiniować zasoby, które zostaną utworzone po wprowadzeniu zakresu i zniszczone po wyjściu z zakresu. Mimo że wciąż brakuje mu poziomu automatyzacji i pewności zapewnionego przez C ++, jest to znacząca poprawa w stosunku do Javy. W szczególności projektant klasy może scentralizować szczegóły tego, jak to zrobićpozbyć się klasy w jej implementacjiIDisposable
. Dla programisty klienckiego pozostaje tylko mniejszy ciężar napisaniausing
instrukcji, aby mieć pewność, żeIDisposable
interfejs zostanie użyty w odpowiednim czasie. W Javie 7 i nowszych nazwy zostały zmienione, aby chronić winnych, ale podstawowa idea jest w zasadzie identyczna.źródło
Nie mogę uwierzyć, że nikt inny nie poruszył to (gra słów nie przeznaczonych) - nie trzeba się łapać klauzulę!
Jest to całkowicie uzasadnione:
Żadna klauzula catch nigdzie nie jest widoczna, ponieważ ta metoda nie może zrobić nic użytecznego z tymi wyjątkami; są pozostawione do propagowania z powrotem stosu wywołań do modułu obsługi, który może . Łapanie i rzucanie wyjątków w każdej metodzie jest złym pomysłem, szczególnie jeśli po prostu rzucasz ten sam wyjątek. Jest to całkowicie sprzeczne z tym, jak powinna działać funkcja obsługi wyjątków strukturalnych (i jest bardzo bliska zwrócenia „kodu błędu” z każdej metody, tylko w „kształcie” wyjątku).
Jednak ta metoda musi się posprzątać, aby „świat zewnętrzny” nigdy nie musiał wiedzieć o bałaganie, w który się wpakował. Ta klauzula robi właśnie to - bez względu na to, jak zachowują się wywoływane metody, klauzula final zostanie wykonana „przy wyjściu” z metody (i to samo dotyczy każdej klauzuli final między punktem, w którym wyjątek jest zgłaszany, a ewentualna klauzula połowowa, która ją obsługuje); każdy z nich jest uruchamiany, gdy stos wywołań „rozwija się”.
źródło
Co by się stało, gdyby zgłoszono wyjątek, którego się nie spodziewałeś. Próba zakończy się w jej środku i nie zostanie wykonana żadna klauzula catch.
Ostatnim blokiem jest pomoc w tym i zapewnienie, że bez względu na wyjątek nastąpi czyszczenie.
źródło
finally
, ponieważ możesz zapobiec „nieoczekiwanym” wyjątkomcatch(Object)
lubcatch(...)
catch-all.Niektóre języki oferują zarówno konstruktory, jak i destruktory dla swoich obiektów (np. C ++, jak sądzę). Za pomocą tych języków możesz zrobić większość (prawdopodobnie wszystkie) tego, co zwykle robi się w
finally
destruktorze. Jako taka - w tych językach -finally
klauzula może być zbędna.W języku bez destruktorów (np. Java) uzyskanie (bez
finally
klauzuli ) trudnego (może nawet niemożliwego) osiągnięcia prawidłowego czyszczenia . NB - W Javie istniejefinalise
metoda, ale nie ma gwarancji, że zostanie kiedykolwiek wywołana.źródło
finalise
ale wolałbym nie wdawać się teraz w polityczne spory wokół destrukcji / finałów.finalise
ale z zarówno rozszerzalnym smakiem, jak i mechanizmem przypominającym oop - bardzo wyrazisty i porównywalny zfinalise
mechanizmem innych języków.Spróbuj w końcu i spróbuj złapać to dwie różne rzeczy, które dzielą tylko słowo kluczowe: „try”. Osobiście chciałbym zobaczyć to inaczej. Powodem, dla którego widzisz je razem, jest to, że wyjątki powodują „skok”.
I spróbuj wreszcie jest zaprojektowany do uruchamiania kodu, nawet jeśli programowanie wyskoczy. Czy to z powodu wyjątku, czy z innego powodu. To fajny sposób na zdobycie surowca i upewnienie się, że został wyczyszczony bez martwienia się o skoki.
źródło
try catch
ale nie obsługujetry finally
; kod używający tego ostatniego jest konwertowany na kod przy użyciu tylko pierwszego, poprzez skopiowanie zawartościfinally
bloku we wszystkich miejscach kodu, w których może być konieczne jego wykonanie.Ponieważ to pytanie nie określa C ++ jako języka, rozważę połączenie C ++ i Javy, ponieważ mają one inne podejście do niszczenia obiektów, co sugeruje się jako jedną z alternatyw.
Powody, dla których możesz użyć bloku wreszcie zamiast kodu po bloku try-catch
wracasz wcześnie z bloku próbnego: Rozważ to
w porównaniu z:
wracasz wcześnie z bloków bloków: porównaj
vs:
Rzucasz wyjątki. Porównać:
vs:
Te przykłady nie sprawiają, że wydaje się to takie złe, ale często masz kilka z tych interakcji i więcej niż jeden wyjątek / typ zasobu w grze.
finally
może pomóc w uniknięciu plątaniny koszmaru utrzymania.Teraz w C ++ można nimi zarządzać za pomocą obiektów opartych na zakresie. Ale IMO ma dwie wady tego podejścia 1. Składnia jest mniej przyjazna. 2. Porządek budowy będący odwrotnością porządku zniszczenia może sprawić, że sprawy staną się mniej jasne.
W Javie nie możesz podpiąć się do metody finalizacji, aby wykonać porządek, ponieważ nie wiesz, kiedy to się stanie - (możesz, ale jest to ścieżka wypełniona zabawnymi warunkami wyścigowymi - JVM ma wiele możliwości decydowania, kiedy zniszczy rzeczy - często nie kiedy się tego spodziewasz - wcześniej lub później, niż możesz się spodziewać - i to może się zmienić w miarę uruchamiania kompilatora hot-spot ... westchnienie ...)
źródło
Wszystko, co jest logicznie „konieczne” w języku programowania, to instrukcje:
Każdy algorytm można zaimplementować tylko przy użyciu powyższych instrukcji, wszystkie inne konstrukcje językowe mają na celu ułatwienie pisania programów i zrozumienie dla innych programistów.
Rzeczywisty sprzęt można zobaczyć na starszym komputerze, korzystając z tak minimalnego zestawu instrukcji.
źródło
W rzeczywistości większa luka dla mnie występuje zwykle w językach, które obsługują,
finally
ale nie zawierają destruktorów, ponieważ można modelować całą logikę związaną z „czyszczeniem” (które podzielę na dwie kategorie) za pomocą destruktorów na poziomie centralnym, bez ręcznego zajmowania się czyszczeniem logika w każdej istotnej funkcji. Gdy widzę, że C # lub kod Java robią takie rzeczy, jak ręczne odblokowywanie muteksów i zamykanie plików wfinally
blokach, wydaje się to nieaktualne i przypomina trochę kod C, gdy wszystko to jest zautomatyzowane w C ++ przez destruktory w sposób, który uwalnia ludzi od tej odpowiedzialności.Jednak nadal znajdowałbym niewielką wygodę, gdyby uwzględniono C ++,
finally
a to dlatego, że istnieją dwa rodzaje porządków:Drugi przynajmniej nie odwzorowuje tak intuicyjnie idei niszczenia zasobów, choć można to zrobić dobrze za pomocą osłon celowniczych, które automatycznie wycofują zmiany, gdy zostaną zniszczone przed popełnieniem. Tam
finally
zapewne zapewnia przynajmniej nieznacznie (tylko o trochę malusieńki) bardziej prosty mechanizm pracy niż strażników zakresu.Jednak jeszcze prostszym mechanizmem byłby
rollback
blok, którego nigdy wcześniej nie widziałem w żadnym języku. To mój wymarzony fajny pomysł, gdybym kiedykolwiek zaprojektował język, który wymagałby obsługi wyjątków. Przypominałoby to:Byłby to najprostszy sposób na modelowanie wycofywania efektów ubocznych, podczas gdy destruktory są właściwie idealnym mechanizmem do oczyszczania lokalnych zasobów. Teraz zapisuje tylko kilka dodatkowych wierszy kodu z rozwiązania straży zasięgu, ale dlatego tak chcę zobaczyć język z tym, że wycofywanie efektów ubocznych jest zwykle najbardziej zaniedbanym (ale najtrudniejszym) aspektem obsługi wyjątków w językach, które obracają się wokół zmienności. Myślę, że ta funkcja zachęciłaby programistów do zastanowienia się nad obsługą wyjątków we właściwy sposób, jeśli chodzi o wycofywanie transakcji, ilekroć funkcje powodują skutki uboczne i nie są ukończone, a jako dodatkowy bonus, gdy ludzie widzą, jak trudno jest cofnąć błędy, mogą przede wszystkim sprzyjać pisaniu większej liczby funkcji wolnych od efektów ubocznych.
Istnieją również niejasne przypadki, w których po prostu chcesz robić różne rzeczy bez względu na to, kiedy wychodzisz z funkcji, bez względu na to, jak ona się zakończyła, na przykład rejestrowanie znacznika czasu. Jest
finally
to prawdopodobnie najbardziej proste i doskonałe rozwiązanie do pracy, ponieważ próbuje instancję obiektu tylko do korzystania z jego destruktora wyłącznie w celu logowania znacznik czasu po prostu czuje się bardzo dziwnie (choć można to zrobić dobrze i całkiem wygodnie z lambdas ).źródło
Podobnie jak wiele innych niezwykłych rzeczy w języku C ++, brak
try/finally
konstrukcji jest wadą projektową, jeśli można nawet nazwać to tak, że w języku, który często wydaje się, że w ogóle nie wykonano żadnej pracy projektowej .RAII (użycie opartego na zakresie deterministycznego wywołania destruktora destrukcyjnego na obiektach opartych na stosie do czyszczenia) ma dwie poważne wady. Po pierwsze, wymaga użycia obiektów opartych na stosie , które są obrzydliwościami, które naruszają zasadę substytucji Liskowa. Istnieje wiele dobrych powodów, dla których żaden inny język OO nie był używany przed ani od C ++ - w epsilon; D nie liczy się, ponieważ opiera się w dużej mierze na C ++ i i tak nie ma udziału w rynku - a wyjaśnienie problemów, które powodują, wykracza poza zakres tej odpowiedzi.
Po drugie,
finally
można zrobić nadzbiór zniszczenia obiektu. Wiele z tego, co dzieje się z RAII w C ++, można opisać w języku Delphi, który nie ma funkcji wyrzucania elementów bezużytecznych, z następującym wzorcem:Jest to wyraźny wzór RAII; gdybyś stworzył procedurę C ++, która zawiera tylko odpowiednik pierwszej i trzeciej linii powyżej, to co wygenerowałby kompilator, wyglądałoby tak, jak napisałem w swojej podstawowej strukturze. A ponieważ jest to jedyny dostęp do
try/finally
konstrukcji, którą zapewnia C ++, programiści C ++ mają raczej krótkowzroczny poglądtry/finally
: kiedy wszystko, co masz, to młotek, wszystko zaczyna wyglądać jak destruktor, że tak powiem.Są jednak inne rzeczy, które doświadczony programista może zrobić z
finally
konstrukcją. Nie chodzi o deterministyczne zniszczenie, nawet w obliczu wyjątku; chodzi o deterministyczne wykonanie kodu , nawet w obliczu wyjątku.Oto kolejna rzecz, którą często można zobaczyć w kodzie Delphi: Obiekt zestawu danych z powiązanymi z nim kontrolkami użytkownika. Zestaw danych przechowuje dane ze źródła zewnętrznego, a elementy sterujące odzwierciedlają stan danych. Jeśli masz zamiar załadować wiązkę danych do zestawu danych, będziesz chciał tymczasowo wyłączyć powiązanie danych, aby nie robiło to dziwnych rzeczy w interfejsie użytkownika, próbując aktualizować go w kółko z każdym nowym wprowadzonym rekordem , więc kodowałbyś to w ten sposób:
Najwyraźniej nie ma tu żadnego obiektu niszczonego i takiego nie trzeba. Kod jest prosty, zwięzły, wyraźny i wydajny.
Jak można to zrobić w C ++? Cóż, najpierw musisz zakodować całą klasę . Prawdopodobnie byłoby to nazwane
DatasetEnabler
lub coś takiego. Całe jego istnienie byłoby jak pomocnik RAII. Następnie musisz zrobić coś takiego:Tak, te pozornie zbędne nawiasy klamrowe są niezbędne do zarządzania właściwym zasięgiem i zapewnienia, że zestaw danych zostanie ponownie włączony natychmiast, a nie na końcu metody. Zatem to, co kończysz, nie zajmuje mniej linii kodu (chyba że używasz nawiasów egipskich). Wymaga utworzenia zbędnego obiektu, który ma narzut. (Czy kod C ++ nie powinien być szybki?) Nie jest jawny, ale opiera się na magii kompilatora. Wykonywany kod nie jest nigdzie opisany w tej metodzie, ale rezyduje w zupełnie innej klasie, być może w zupełnie innym pliku . Krótko mówiąc, nie jest to w żaden sposób lepsze rozwiązanie niż samodzielne napisanie
try/finally
bloku.Ten rodzaj problemu jest na tyle powszechny w projektowaniu języka, że istnieje jego nazwa: inwersja abstrakcji. Występuje, gdy konstrukcja wysokiego poziomu jest budowana na szczycie konstrukcji niskiego poziomu, a następnie konstrukcja niskiego poziomu nie jest bezpośrednio obsługiwana w języku, wymagając od tych, którzy chcą go użyć, ponownego wdrożenia go pod względem konstrukcja wysokiego poziomu, często za wysokie kary, zarówno w zakresie czytelności kodu, jak i wydajności.
źródło