Czy „wreszcie” część konstrukcji „spróbuj… złapać… wreszcie” jest nawet konieczna?

25

Niektóre języki (takie jak C ++ i wcześniejsze wersje PHP) nie obsługują finallyczęści try ... catch ... finallykonstruktu. Czy finallykiedykolwiek jest to konieczne? Ponieważ kod w nim zawsze działa, dlaczego nie powinienem / nie powinienem po prostu umieszczać tego kodu po try ... catchbloku bez finallyklauzuli? Dlaczego warto skorzystać (Szukam powodu / motywacji do używania / nieużywania finally, nie ma powodu, aby pozbyć się „catch” lub dlaczego jest to legalne.)

Agi Hammerthief
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
wałek klonowy

Odpowiedzi:

36

Oprócz tego, co powiedzieli inni, możliwe jest również wprowadzenie wyjątku w klauzuli catch. Rozważ to:

try { 
    throw new SomeException();
} catch {
    DoSomethingWhichUnexpectedlyThrows();
}
Cleanup();

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.

Erik
źródło
4
Dziękujemy za zwięzłą i bezpośrednią odpowiedź, która nie zagłębia się w teorię, a terytorium „język X jest lepszy niż Y”.
Agi Hammerthief
56

Jak wspomnieli inni, nie ma gwarancji, że kod po tryinstrukcji zostanie wykonany, chyba że złapiesz każdy możliwy wyjątek. To powiedziawszy to:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   handleError();
} finally {
   cleanUp();
}

można przepisać 1 jako:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   try {
       handleError();
   } catch (Throwable e2) {
       cleanUp();
       throw e2;
   }
} catch (Throwable e) {
   cleanUp();
   throw e;
}
cleanUp();

Ale ta ostatnia wymaga wychwycenia wszystkich nieobsługiwanych wyjątków, zduplikowania kodu czyszczenia i pamiętania o ponownym rzuceniu. Nie finallyjest to konieczne , ale jest przydatne .

C ++ nie ma, finallyponieważ Bjarne Stroustrup uważa, że ​​RAII jest lepszy lub przynajmniej wystarcza w większości przypadków:

Dlaczego C ++ nie zapewnia konstrukcji „nareszcie”?

Ponieważ C ++ obsługuje alternatywę, która jest prawie zawsze lepsza: technika „pozyskiwania zasobów to inicjalizacja” (TC ++ PL3 sekcja 14.4). Podstawową ideą jest reprezentowanie zasobu przez obiekt lokalny, aby destruktor obiektu lokalnego zwolnił zasób. W ten sposób programista nie może zapomnieć o zwolnieniu zasobu.


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;.

Doval
źródło
8
Musisz także wychwycić wyjątki w handleError()drugim przypadku, nie?
Juri Robl
1
Możesz także zgłaszać błąd. Chciałbym przeformułować to catch (Throwable t) {}, próbując złapać blok wokół całego początkowego bloku (aby również złapać przedmioty do rzucania handleError)
njzk2
1
Dodałbym dodatkowy try-catch, który pominąłeś podczas wywoływania, 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).
Alex
1
Ta odpowiedź tak naprawdę nie odpowiada na pytanie, dlaczego nie ma C ++ finally, co jest o wiele bardziej dopracowane.
DeadMG
1
@AgiHammerthief Zagnieżdżony tryjest wewnątrz catchdla 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 zagwarantowanie cleanUpuruchomienia jest wyłapanie wszystkiego , ale oryginalny kod zezwalałby na wyjątki pochodzące z catch (SpecificException e)bloku na propagowanie w górę.
Doval
22

finally bloki są zwykle używane do czyszczenia zasobów, co może pomóc w czytelności przy użyciu wielu instrukcji return:

int DoSomething() {
    try {
        open_connection();
        return get_result();
    }
    catch {
        return 2;
    }
    finally {
        close_connection();
    }
}

vs

int DoSomething() {
    int result;
    try {
        open_connection();
        result = get_result();
    }
    catch {
        result = 2;
    }
    close_connection();
    return result;
}
AlexFoxGill
źródło
2
Myślę, że to najlepsza odpowiedź. Używanie wreszcie jako alternatywy dla ogólnego wyjątku wydaje się po prostu gówniane. Poprawnym przypadkiem użycia jest czyszczenie zasobów lub analogiczne operacje.
Kik
3
Być może jeszcze bardziej powszechne jest wracanie do bloku try, niż do bloku catch.
Michael Anderson
Moim zdaniem kod nie wyjaśnia w odpowiedni sposób użycia finally. (
Użyłbym
15

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/ finallynie 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/ finallyzapewnia 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ę, finalizektó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:

class Foo {
    // ...
public:
    void do_whatever() { if (xyz) throw something; }
    ~Foo() { /* handle cleanup */ }
};

... a kod klienta wygląda mniej więcej tak:

void f() { 
    Foo f;
    f.do_whatever();
    // possibly more code that might throw here
}

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/ finallylub 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ć IDisposableinterfejs, który zapewnia Disposemetodę (przynajmniej niejasną) podobną do destruktora C ++. Podczas gdy można tego użyć za pomocą try/ finallyjak w Javie, C # nieco automatyzuje zadanie za pomocą usinginstrukcji, 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 implementacji IDisposable. Dla programisty klienckiego pozostaje tylko mniejszy ciężar napisania usinginstrukcji, aby mieć pewność, że IDisposableinterfejs 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.

Jerry Coffin
źródło
1
Idealna odpowiedź. Destruktory są cecha trzeba mieć w C ++.
Thomas Eding
13

Nie mogę uwierzyć, że nikt inny nie poruszył to (gra słów nie przeznaczonych) - nie trzeba się łapać klauzulę!

Jest to całkowicie uzasadnione:

try 
{
   AcquireManyResources(); 
   DoSomethingThatMightFail(); 
}
finally 
{
   CleanUpThoseResources(); 
}

Ż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ę”.

Phill W.
źródło
9

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.

maniak zapadkowy
źródło
4
Nie jest to wystarczający powód finally, ponieważ możesz zapobiec „nieoczekiwanym” wyjątkom catch(Object)lub catch(...)catch-all.
MSalters
1
Brzmi jak obejście. Koncepcyjnie wreszcie jest czystszy. Chociaż muszę przyznać, że rzadko go używam.
szybko_nie
7

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 finallydestruktorze. Jako taka - w tych językach - finallyklauzula może być zbędna.

W języku bez destruktorów (np. Java) uzyskanie (bez finallyklauzuli ) trudnego (może nawet niemożliwego) osiągnięcia prawidłowego czyszczenia . NB - W Javie istnieje finalisemetoda, ale nie ma gwarancji, że zostanie kiedykolwiek wywołana.

OldCurmudgeon
źródło
Warto zauważyć, że destruktory pomagają w czyszczeniu zasobów, gdy zniszczenie jest deterministyczne . Jeśli nie wiemy, kiedy obiekt zostanie zniszczony i / lub wyrzucony do kosza, wówczas destruktory nie są wystarczająco bezpieczne.
Morwenn
@Morwenn - Dobra uwaga. Wskazałem na to, odnosząc się do Javy, finaliseale wolałbym nie wdawać się teraz w polityczne spory wokół destrukcji / finałów.
OldCurmudgeon
W C ++ zniszczenie jest deterministyczne. Kiedy zakres zawierający automatyczny obiekt wychodzi (np. Zostaje zdjęty ze stosu), wywoływany jest jego destruktor. (C ++ pozwala alokować obiekty na stosie, a nie tylko na stosie).
Rob K
@RobK - I to jest dokładna funkcjonalność, finaliseale z zarówno rozszerzalnym smakiem, jak i mechanizmem przypominającym oop - bardzo wyrazisty i porównywalny z finalisemechanizmem innych języków.
OldCurmudgeon
1

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.

Pieter B.
źródło
3
W .NET są implementowane przy użyciu oddzielnych mechanizmów; jednak w Javie jedyna konstrukcja rozpoznawana przez JVM jest semantycznie równoważna „na błąd goto”, wzorzec, który bezpośrednio obsługuje, try catchale nie obsługuje try finally; kod używający tego ostatniego jest konwertowany na kod przy użyciu tylko pierwszego, poprzez skopiowanie zawartości finallybloku we wszystkich miejscach kodu, w których może być konieczne jego wykonanie.
supercat
@ superuper ładne, dzięki za dodatkowe informacje o Javie.
Pieter B
1

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

    Database db = null;
    try {
     db = open_database();
     if(db.isSomething()) {
       return 7;
     }
     return db.someThingElse();
    } finally {
      if(db!=null)
        db.close();
    }
    

    w porównaniu z:

    Database db = null;
    int returnValue = 0;
    try {
     db = open_database();
     if(db.isSomething()) {
       returnValue = 7;
     } else {
       returnValue = db.someThingElse();
     }
    } catch(Exception e) {
      if(db!=null)
        db.close();
    }
    return returnValue;
    
  • wracasz wcześnie z bloków bloków: porównaj

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      return 7;
    } catch (DBIsADonkeyException e ) {
      return 11;
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null) 
        db.close();
      return 7;
    } catch (DBIsADonkeyException e ) {
      if(db!=null)
        db.close();
      return 11;
    }           
    db.close();
    
  • Rzucasz wyjątki. Porównać:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      throw convertToRuntimeException(e,"DB was wonkey");
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null)
        db.close();
      throw convertToRuntimeException(e,"DB was wonkey");
    } 
    if(db!=null)
      db.close();
    

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. finallymoż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 ...)

Michael Anderson
źródło
1

Wszystko, co jest logicznie „konieczne” w języku programowania, to instrukcje:

assignment a = b
subtract a from b
goto label
test a = 0
if true goto label

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.

James Anderson
źródło
1
Twoja odpowiedź jest z pewnością prawdziwa, ale nie koduję w asemblerze; to jest zbyt bolesne. Pytam, dlaczego warto skorzystać z funkcji, która nie ma sensu w językach, które ją obsługują, a nie jaki jest kompletny minimalny zestaw instrukcji języka.
Agi Hammerthief
1
Chodzi o to, że każdy język implementujący tylko te 5 operacji może zaimplementować dowolny algorytm - choć dość kręto. Większość operatorów vers / w językach wysokiego poziomu nie jest „konieczna”, jeśli celem jest jedynie wdrożenie algorytmu. Jeśli celem jest szybki rozwój czytelnego, możliwego do utrzymania kodu, większość jest konieczna, ale „czytelne” i „łatwe do utrzymania” nie są mierzalne i wyjątkowo subiektywne. Mili programiści języków wprowadzili wiele funkcji: jeśli nie masz zastosowania do niektórych z nich, nie używaj ich.
James Anderson
0

W rzeczywistości większa luka dla mnie występuje zwykle w językach, które obsługują, finallyale 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 w finallyblokach, 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 ++, finallya to dlatego, że istnieją dwa rodzaje porządków:

  1. Niszczenie / uwalnianie / odblokowywanie / zamykanie / itp. Lokalnych zasobów (destruktory są do tego idealne).
  2. Cofanie / cofanie zewnętrznych efektów ubocznych (do tego wystarczają destruktory).

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 finallyzapewne zapewnia przynajmniej nieznacznie (tylko o trochę malusieńki) bardziej prosty mechanizm pracy niż strażników zakresu.

Jednak jeszcze prostszym mechanizmem byłby rollbackblok, 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:

try
{
    // Cause external side effects. These side effects should
    // be undone if we don't finish successfully.
}
rollback
{
    // Reverse external side effects. This block is *only* executed 
    // if the 'try' block above faced a premature return out 
    // of the function. It is different from 'finally' which 
    // gets executed regardless of whether or not the function 
    // exited prematurely. This block *only* gets executed if we 
    // exited prematurely from  the try block so that we can undo 
    // whatever side effects it failed to finish making. If the try 
    // block succeeded and didn't face a premature exit, then we 
    // don't want this block to execute.
}

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 finallyto 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
-9

Podobnie jak wiele innych niezwykłych rzeczy w języku C ++, brak try/finallykonstrukcji 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, finallymoż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:

myObject := MyClass.Create(arguments);
try
   doSomething(myObject);
finally
   myObject.Free();
end;

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/finallykonstrukcji, którą zapewnia C ++, programiści C ++ mają raczej krótkowzroczny pogląd try/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 finallykonstrukcją. 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:

dataset.DisableControls();
try
   LoadData(dataset);
finally
   dataset.EnableControls();
end;

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 DatasetEnablerlub coś takiego. Całe jego istnienie byłoby jak pomocnik RAII. Następnie musisz zrobić coś takiego:

dataset.DisableControls();
{
   raiiGuard = DatasetEnabler(dataset);
   LoadData(dataset);
}

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/finallybloku.

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.

Mason Wheeler
źródło
Komentarze mają na celu wyjaśnienie lub ulepszenie pytania i odpowiedzi. Jeśli chcesz porozmawiać o tej odpowiedzi, przejdź do pokoju rozmów. Dziękuję Ci.
wałek klonowy