Wiele współczesnych języków zapewnia bogate funkcje obsługi wyjątków , ale język programowania Swift firmy Apple nie zapewnia mechanizmu obsługi wyjątków .
Pogrążony w wyjątkach, tak jak ja, mam problem z otuleniem się, co to znaczy. Swift ma asercje i oczywiście zwraca wartości; ale mam problem z wyobrażeniem sobie, w jaki sposób moje myślenie oparte na wyjątkach mapuje świat bez wyjątków (i, w takim razie, dlaczego taki świat jest pożądany ). Czy są rzeczy, których nie mogę zrobić w języku takim jak Swift, które mogę zrobić z wyjątkami? Czy coś zyskuję tracąc wyjątki?
Jak na przykład najlepiej wyrazić coś takiego
try:
operation_that_can_throw_ioerror()
except IOError:
handle_the_exception_somehow()
else:
# we don't want to catch the IOError if it's raised
another_operation_that_can_throw_ioerror()
finally:
something_we_always_need_to_do()
w języku (na przykład Swift), który nie obsługuje wyjątków?
panic
to, co nie jest takie samo. Oprócz tego, co tam powiedziano, wyjątek nie jest niczym więcej niż wyrafinowanym (ale wygodnym) sposobem na wykonanieGOTO
, choć z oczywistych powodów, nikt tak go nie nazywa.Odpowiedzi:
W programowaniu wbudowanym wyjątki tradycyjnie nie były dozwolone, ponieważ narzut związany z odwijaniem stosu, który musisz zrobić, został uznany za niedopuszczalną zmienność podczas próby utrzymania wydajności w czasie rzeczywistym. Chociaż technicznie smartfony można uznać za platformy czasu rzeczywistego, są one teraz wystarczająco mocne, gdy stare ograniczenia systemów wbudowanych już tak naprawdę nie obowiązują. Po prostu podnoszę to ze względu na dokładność.
Wyjątki są często obsługiwane w funkcjonalnych językach programowania, ale są tak rzadko stosowane, że równie dobrze mogą nie być. Jednym z powodów jest leniwa ocena, która jest czasami przeprowadzana nawet w językach, które nie są domyślnie leniwe. Posiadanie funkcji, która wykonuje się na innym stosie niż miejsce, w którym została wykonana w kolejce, utrudnia określenie, gdzie umieścić program obsługi wyjątków.
Innym powodem jest to, że funkcje pierwszej klasy pozwalają na konstrukcje takie jak opcje i kontrakty terminowe, które zapewniają większą elastyczność dzięki wyjątkom składniowym. Innymi słowy, reszta języka jest na tyle wyrazista, że wyjątki nic ci nie kupują.
Swift nie jest mi obyty, ale trochę przeczytałem o jego obsłudze błędów sugeruje, że zamierzali obsługiwać błędy, by podążać za wzorcami bardziej funkcjonalnymi. Widziałem przykłady kodu z blokami
success
i,failure
które wyglądają bardzo podobnie do futures.Oto przykład przy użyciu
Future
z tym tutorialu Scala :Możesz zobaczyć, że ma mniej więcej taką samą strukturę jak twój przykład z wyjątkami.
future
Blok jest niczymtry
.onFailure
Blok jest jak obsługi wyjątków. W Scali, podobnie jak w większości języków funkcjonalnych,Future
jest wdrażany całkowicie przy użyciu samego języka. Nie wymaga żadnej specjalnej składni, jak wyjątki. Oznacza to, że możesz zdefiniować własne podobne konstrukcje. Możetimeout
na przykład dodać blok lub funkcję rejestrowania.Dodatkowo możesz przekazywać przyszłość, zwracać ją z funkcji, przechowywać w strukturze danych lub cokolwiek innego. To wartość pierwszej klasy. Nie jesteś ograniczony jak wyjątki, które muszą być propagowane bezpośrednio na stosie.
Opcje rozwiązują problem obsługi błędów w nieco inny sposób, co działa lepiej w niektórych przypadkach użycia. Nie utkniesz tylko z jedną metodą.
Takie rzeczy „zyskujesz, tracąc wyjątki”.
źródło
Future
jest w istocie sposobem rozpatrywania wartość zwracana z funkcji, bez zatrzymywania się czekać na niego. Podobnie jak Swift, opiera się na wartości zwracanej, ale w przeciwieństwie do Swift, odpowiedź na wartość zwracaną może wystąpić później (trochę jak wyjątki). Dobrze?Future
poprawnie, ale myślę, że możesz źle opisywać Swift. Zobacz na przykład pierwszą część odpowiedzi dotyczącej przepełnienia stosu.Either
byłby lepszym przykładem IMHOWyjątki mogą utrudnić zrozumienie kodu. Chociaż nie są tak potężne jak gotos, mogą powodować wiele takich samych problemów z powodu ich nielokalnego charakteru. Załóżmy na przykład, że masz fragment kodu imperatywnego takiego jak ten:
Na pierwszy rzut oka nie można stwierdzić, czy którakolwiek z tych procedur może zgłosić wyjątek. Musisz to przeczytać w dokumentacji każdej z tych procedur. (Niektóre języki ułatwiają to nieco poprzez rozszerzenie podpisu o te informacje). Powyższy kod będzie dobrze kompilował się niezależnie od tego, czy którakolwiek z procedur zostanie wygenerowana, dzięki czemu naprawdę łatwo zapomnieć o obsłudze wyjątku.
Ponadto, nawet jeśli celem jest propagowanie wyjątku z powrotem do osoby dzwoniącej, często trzeba dodać dodatkowy kod, aby zapobiec pozostawieniu rzeczy w niespójnym stanie (np. Jeśli ekspres do kawy się zepsuje, nadal musisz wyczyścić bałagan i wrócić kubek!). Tak więc w wielu przypadkach kod wykorzystujący wyjątki wyglądałby tak samo skomplikowany jak kod, który nie zrobiłby tego ze względu na dodatkowe wymagane czyszczenie.
Wyjątki można emulować za pomocą wystarczająco wydajnego systemu typów. Wiele języków, które unikają wyjątków, używa wartości zwracanych, aby uzyskać takie samo zachowanie. Jest podobny do tego w C, ale nowoczesne systemy typu zwykle sprawiają, że jest bardziej elegancki, a także trudniej zapomnieć o obsłudze błędu. Mogą również dostarczać cukier syntaktyczny, aby uczynić rzeczy mniej uciążliwymi, czasami prawie tak czystymi, jak by to było z wyjątkami.
W szczególności, poprzez osadzenie obsługi błędów w systemie typów zamiast implementacji jako osobnej funkcji, można zastosować „wyjątki” dla innych rzeczy, które nawet nie są związane z błędami. (Dobrze wiadomo, że obsługa wyjątków jest w rzeczywistości własnością monad.)
źródło
goto
:goto
Ogranicza się do pojedynczej funkcji, która jest dość małym zakresem, pod warunkiem, że funkcja jest naprawdę niewielka, wyjątek działa bardziej jak krzyżówkagoto
icome from
(patrz en.wikipedia.org/wiki/INTERCAL ;-)). Może w zasadzie łączyć dowolne dwa fragmenty kodu, prawdopodobnie pomijając niektóre trzecie funkcje. Jedyne, czego nie może zrobić, cogoto
może, to powrót.Jest tu kilka świetnych odpowiedzi, ale myślę, że jeden ważny powód nie został wystarczająco podkreślony: kiedy zdarzają się wyjątki, obiekty mogą pozostać w nieprawidłowych stanach. Jeśli możesz „złapać” wyjątek, kod obsługi wyjątków będzie mógł uzyskać dostęp do tych nieprawidłowych obiektów i pracować z nimi. To pójdzie okropnie źle, chyba że kod dla tych obiektów zostanie napisany idealnie, co jest bardzo, bardzo trudne do zrobienia.
Wyobraź sobie na przykład implementację Vector. Jeśli ktoś utworzy instancję urządzenia Vector z zestawem obiektów, ale podczas inicjowania wystąpi wyjątek (być może, powiedzmy, podczas kopiowania obiektów do nowo przydzielonej pamięci), bardzo trudno jest poprawnie zakodować implementację Vector w taki sposób, aby nie pamięć wyciekła. Ten krótki artykuł autorstwa Stroustroup obejmuje przykład Vector .
A to tylko wierzchołek góry lodowej. Co jeśli na przykład skopiowałeś niektóre elementy, ale nie wszystkie z nich? Aby poprawnie zaimplementować kontener, taki jak Vector, prawie wszystkie operacje, które podejmujesz, muszą być odwracalne, więc cała operacja ma charakter atomowy (jak transakcja w bazie danych). Jest to skomplikowane i większość aplikacji źle to robi. I nawet jeśli jest to zrobione poprawnie, znacznie komplikuje proces wdrażania kontenera.
Dlatego niektóre współczesne języki zdecydowały, że nie warto. Na przykład Rust ma wyjątki, ale nie można ich „złapać”, więc nie ma możliwości interakcji kodu z obiektami w niepoprawnym stanie.
źródło
Jedną rzeczą, którą początkowo byłem zaskoczony językiem Rust, jest to, że nie obsługuje wyjątków catch. Możesz zgłaszać wyjątki, ale tylko środowisko wykonawcze może je wychwycić, gdy umiera zadanie (pomyśl wątek, ale nie zawsze osobny wątek systemu operacyjnego); jeśli sam zaczniesz zadanie, możesz zapytać, czy zakończyło się normalnie, czy też zostało
fail!()
edytowane.Jako taki
fail
często nie jest idiomatyczny . Kilka przypadków, w których tak się dzieje, to na przykład uprząż testowa (która nie wie, jaki jest kod użytkownika), jako najwyższy poziom kompilatora (większość kompilatorów zamiast tego wykonuje rozwidlenie) lub podczas wywoływania wywołania zwrotnego na wejście użytkownika.Zamiast wspólnej idiom jest użycie ten
Result
szablon jawnie przepuścić błędy, które powinny być traktowane. Jest to znacznie łatwiejsze, natry!
makro , który może być owinięty wokół każdego ekspresję, który daje w wyniku i plonów pomyślne ramię jeżeli istnieje, albo w przeciwnym przypadku przejście wcześniej z tej funkcji.źródło
assert
, ale niecatch
?Moim zdaniem wyjątki są niezbędnym narzędziem do wykrywania błędów kodu w czasie wykonywania. Zarówno w testach, jak i podczas produkcji. Spraw, aby ich wiadomości były wystarczająco szczegółowe, aby w połączeniu ze śledzeniem stosu można było dowiedzieć się, co się stało z dziennika.
Wyjątki stanowią głównie narzędzie programistyczne i sposób uzyskiwania rozsądnych raportów o błędach z produkcji w nieoczekiwanych przypadkach.
Oprócz oddzielenia problemów (szczęśliwa ścieżka z oczekiwanymi błędami w porównaniu do wpadnięcia do jakiegoś ogólnego programu obsługi nieoczekiwanych błędów) jest dobra rzecz, dzięki czemu twój kod jest bardziej czytelny i łatwy w utrzymaniu, w rzeczywistości niemożliwe jest przygotowanie kodu na wszystkie możliwe nieoczekiwane przypadki, nawet nadmuchując go kodem obsługi błędów, aby zakończyć nieczytelność.
To właściwie oznacza „nieoczekiwany”.
Btw., Czego się spodziewać, a czego nie, to decyzja, którą można podjąć tylko na stronie połączenia. Dlatego sprawdzone wyjątki w Javie nie zadziałały - decyzja jest podejmowana w momencie tworzenia API, kiedy nie jest jasne, czego się spodziewać lub nieoczekiwanego.
Prosty przykład: interfejs API mapy skrótów może mieć dwie metody:
i
pierwszy zgłasza wyjątek, jeśli go nie znaleziono, ten drugi daje wartość opcjonalną. W niektórych przypadkach ten drugi ma większy sens, ale w innych kod musi po prostu oczekiwać wartości dla danego klucza, więc jeśli go nie ma, jest to błąd, którego ten kod nie może naprawić, ponieważ podstawowy założenie się nie powiodło. W tym przypadku pożądane jest wypadnięcie ze ścieżki kodu i przejście do jakiejś ogólnej procedury obsługi w przypadku niepowodzenia wywołania.
Kod nigdy nie powinien próbować radzić sobie z nieudanymi podstawowymi założeniami.
Oczywiście z wyjątkiem sprawdzania ich i rzucania dobrze czytelnych wyjątków.
Rzucanie wyjątków nie jest złem, ale ich złapanie może być. Nie próbuj naprawiać nieoczekiwanych błędów. Złap wyjątki w kilku miejscach, w których chcesz kontynuować pętlę lub operację, zaloguj je, być może zgłoś nieznany błąd i to wszystko.
Bloki połowowe w tym miejscu to bardzo zły pomysł.
Zaprojektuj swoje interfejsy API w taki sposób, aby ułatwić wyrażenie swojej woli, tj. Oświadczenie, czy oczekujesz określonego przypadku, na przykład brak klucza, czy nie. Użytkownicy Twojego interfejsu API mogą wtedy wybrać wywołanie wyrzucające tylko w naprawdę nieoczekiwanych przypadkach.
Sądzę, że powodem, dla którego ludzie nie lubią wyjątków i idą za daleko, pomijając to kluczowe narzędzie do automatyzacji obsługi błędów i lepszego oddzielania problemów od nowych języków, są złe doświadczenia.
To i pewne nieporozumienia na temat tego, do czego są naprawdę dobre.
Symulacja ich poprzez wykonanie WSZYSTKIEGO poprzez monadowe wiązanie sprawia, że kod jest mniej czytelny i łatwy do utrzymania, a ty kończysz się bez śladu stosu, co pogarsza to podejście.
Obsługa błędów funkcjonalnych jest świetna w przypadku oczekiwanych błędów.
Niech obsługa wyjątków automatycznie zajmie się całą resztą, właśnie po to :)
źródło
Swift stosuje tutaj te same zasady, co Cel C, a tym bardziej konsekwentnie. W celu C wyjątki wskazują na błędy programowania. Nie są obsługiwane przez narzędzia do zgłaszania awarii. „Obsługa wyjątków” odbywa się poprzez naprawienie kodu. (Istnieje kilka wyjątków. Na przykład w komunikacji międzyprocesowej. Jest to jednak dość rzadkie i wiele osób nigdy na nią nie natrafia. A Objective-C faktycznie próbuje / łapie / w końcu / rzuca, ale rzadko ich używasz). Swift właśnie usunął możliwość wyłapywania wyjątków.
Swift ma funkcję, która wygląda jak obsługa wyjątków, ale jest po prostu wymuszona obsługa błędów. Historycznie, Objective-C miał dość powszechny wzorzec obsługi błędów: metoda zwracała BOOL (TAK dla sukcesu) lub odwołanie do obiektu (zero dla niepowodzenia, nie zero dla sukcesu) i miała parametr „wskaźnik do NSError *” który byłby używany do przechowywania odwołania NSError. Swift automatycznie przekształca wywołania takiej metody w coś, co wygląda jak obsługa wyjątków.
Ogólnie rzecz biorąc, funkcje Swift mogą łatwo zwracać alternatywy, na przykład wynik, jeśli funkcja działała dobrze i błąd, jeśli się nie powiódł; znacznie ułatwia to obsługę błędów. Ale odpowiedź na pierwotne pytanie: projektanci Swift najwyraźniej uważali, że stworzenie bezpiecznego języka i pisanie udanego kodu w takim języku jest łatwiejsze, jeśli język nie ma wyjątków.
źródło
W C skończyłbyś z czymś takim jak wyżej.
Nie, nic nie ma. Po prostu obsługujesz kody wynikowe zamiast wyjątków.
Wyjątki pozwalają na reorganizację kodu, tak aby obsługa błędów była oddzielna od kodu szczęśliwej ścieżki, ale o to chodzi.
źródło
...throw_ioerror()
zwracania błędów zamiast zgłaszania wyjątków?Result<T, E>
wyliczenie, które może być alboOk<T>
, albo anErr<E>
, zT
typem, który chcesz, jeśli istnieje, iE
typem reprezentującym błąd. Dopasowywanie wzorców i niektóre szczególne metody upraszczają obsługę zarówno sukcesów, jak i niepowodzeń. Krótko mówiąc, nie zakładaj, że brak wyjątków automatycznie oznacza kody błędów.Oprócz odpowiedzi Charliego:
Te przykłady zadeklarowanej obsługi wyjątków, które można znaleźć w wielu instrukcjach i książkach, wyglądają bardzo elegancko tylko na bardzo małych przykładach.
Nawet jeśli odłożysz na bok argument o nieprawidłowym stanie obiektu, zawsze powodują one naprawdę ogromny ból podczas obsługi dużej aplikacji.
Na przykład, gdy masz do czynienia z IO, używając niektórych kryptografii, możesz mieć 20 rodzajów wyjątków, które można wyrzucić z 50 metod. Wyobraź sobie, ile kodu obsługi wyjątków potrzebujesz. Obsługa wyjątków zajmie kilka razy więcej kodu niż sam kod.
W rzeczywistości wiesz, kiedy wyjątek nie może się pojawić, i po prostu nigdy nie musisz pisać tak dużo obsługi wyjątków, więc po prostu zastosuj obejścia, aby zignorować zadeklarowane wyjątki. W mojej praktyce tylko około 5% deklarowanych wyjątków wymaga obsługi kodu, aby mieć niezawodną aplikację.
źródło