Chcę dodać obsługę błędów do:
var firstVariable = 1;
var secondVariable = firstVariable;
Poniższe informacje nie zostaną skompilowane:
try
{
var firstVariable = 1;
}
catch {}
try
{
var secondVariable = firstVariable;
}
catch {}
Dlaczego blok try catch musi wpływać na zakres zmiennych, podobnie jak inne bloki kodu? Odkładając na bok spójność, czy nie ma sensu pakować naszego kodu w obsługę błędów bez konieczności refaktoryzacji?
c#
.net
error-handling
language-features
JᴀʏMᴇᴇ
źródło
źródło
try.. catch
jest specyficznym typem bloku kodu i jeśli chodzi o wszystkie bloki kodu, nie można zadeklarować zmiennej w jednym i używać tej samej zmiennej w innym ze względu na zakres.{}
bez próby.Odpowiedzi:
Co jeśli twój kod to:
Teraz będziesz próbował użyć niezadeklarowanej zmiennej (
firstVariable
), jeśli wywołane zostanie wywołanie metody.Uwaga : powyższy przykład odpowiada konkretnie na pierwotne pytanie, które stwierdza „na bok spójność”. To pokazuje, że istnieją powody inne niż spójność. Ale jak pokazuje odpowiedź Piotra, istnieje również mocny argument konsekwentności, który z pewnością byłby bardzo ważnym czynnikiem przy podejmowaniu decyzji.
źródło
switch
ai uzyskać do nich dostęp w innych.) Ta reguła może łatwo zastosować się tutaj i uniemożliwić kompilację tego kodu. Myślę, że odpowiedź Petera poniżej jest bardziej wiarygodna.catch
bloku, a następnie na pewno zostanie przypisany do drugiegotry
bloku.Wiem, że Ben na to dobrze odpowiedział, ale chciałem zająć się kwestią spójności POV, którą wygodnie odsunięto na bok. Zakładając, że
try/catch
bloki nie wpłynęły na zasięg, to skończyłoby się to:I dla mnie to zderza się z zasadą najmniejszego zdziwienia (POLA), ponieważ teraz masz
{
i}
pełnisz podwójną funkcję w zależności od kontekstu tego, co je poprzedziło.Jedynym wyjściem z tego bałaganu jest wyznaczenie innego znacznika do wyznaczenia
try/catch
bloków. Który zaczyna dodawać zapach kodu. Do czasu, gdy będziesz mieć język bez scopówtry/catch
, byłby to taki bałagan, że lepiej byłoby, gdybyś miał wersję z lunetą.źródło
try
/catch
blokowania”. - masz na myślitry { { // scope } }
:? :){}
ciągnięcie podwójnego obowiązku jako zakresu, a nie tworzenie zakresu w zależności od kontekstu.try^ //no-scope ^
byłby przykładem innego markera.Aby odpowiedzieć na to pytanie, konieczne jest przyjrzenie się nie tylko zakresowi zmiennej .
Nawet jeśli zmienna pozostanie w zakresie, nie zostanie definitywnie przypisana .
Deklaracja zmiennej w bloku try wyraża - dla kompilatora i dla czytelników - że ma ona znaczenie tylko w tym bloku. Wymusza to kompilator.
Jeśli chcesz, aby zmienna była w zasięgu po bloku try, możesz zadeklarować ją poza blokiem:
Wyraża to, że zmienna może mieć znaczenie poza blokiem try. Kompilator pozwoli na to.
Ale pokazuje także inny powód, dla którego zwykle nie byłoby użyteczne utrzymywanie zmiennych w zakresie po wprowadzeniu ich w bloku try. Kompilator C # wykonuje analizę przypisania określonego i zabrania odczytu wartości zmiennej, której nie udowodniono, że została podana wartość. Więc nadal nie możesz odczytać ze zmiennej.
Załóżmy, że próbuję odczytać ze zmiennej po bloku try:
To da błąd podczas kompilacji :
Zadzwoniłem Environment.Exit w bloku catch, tak ja wiem zmienna została przypisana przed wywołaniem Console.WriteLine. Ale kompilator nie wnioskuje o tym.
Dlaczego kompilator jest tak rygorystyczny?
Nie mogę nawet tego zrobić:
Jednym ze sposobów spojrzenia na to ograniczenie jest stwierdzenie, że analiza przypisania określonego w języku C # nie jest bardzo skomplikowana. Ale innym sposobem na to jest to, że kiedy piszesz kod w bloku try z klauzulami catch, mówisz zarówno kompilatorowi, jak i wszystkim ludzkim czytelnikom, że należy go traktować tak, jakby nie wszyscy byli w stanie uruchomić.
Aby zilustrować, co mam na myśli, wyobraź sobie, czy kompilator dopuścił powyższy kod, ale następnie dodałeś wywołanie w bloku try do funkcji , o której osobiście wiesz, że nie zgłasza wyjątku . Nie będąc w stanie zagwarantować, że wywołana funkcja nie wyrzuci an
IOException
, kompilator nie mógł wiedzieć, żen
został przypisany, a następnie trzeba by było dokonać refaktoryzacji.Oznacza to, że rezygnując z wysoce wyrafinowanej analizy w celu ustalenia, czy zmienna przypisana w bloku try z klauzulami catch została definitywnie przypisana później, kompilator pomaga uniknąć pisania kodu, który może później ulec uszkodzeniu. (W końcu złapanie wyjątku zwykle oznacza, że uważasz, że ktoś może zostać rzucony).
Możesz upewnić się, że zmienna jest przypisana przez wszystkie ścieżki kodu.
Kod można skompilować, nadając zmiennej wartość przed blokiem try lub w bloku catch. W ten sposób nadal będzie inicjalizowany lub przypisany, nawet jeśli przypisanie w bloku try nie nastąpi. Na przykład:
Lub:
Te się kompilują. Ale najlepiej zrobić coś takiego, jeśli podana wartość domyślna ma sens * i zapewnia prawidłowe zachowanie.
Zauważ, że w tym drugim przypadku, w którym przypisujesz zmienną w bloku try i we wszystkich blokach catch, chociaż możesz odczytać zmienną po try-catch, nadal nie będziesz w stanie odczytać zmiennej wewnątrz dołączonego
finally
bloku , ponieważ wykonanie może pozostawić blok próbny w większej liczbie sytuacji, niż się często wydaje .* Nawiasem mówiąc, niektóre języki, takie jak C i C ++, oba dopuszczają niezainicjowane zmienne i nie mają określonej analizy przypisań, aby zapobiec ich odczytaniu. Ponieważ odczyt niezainicjowanej pamięci powoduje, że programy zachowują się w sposób niedeterministyczny i nieregularny , generalnie zaleca się unikanie wprowadzania zmiennych w tych językach bez podawania inicjatora. W językach z definitywną analizą przypisań, takich jak C # i Java, kompilator chroni Cię przed czytaniem niezainicjowanych zmiennych, a także mniejszym złem inicjowania ich bezwartościowymi wartościami, które później można błędnie interpretować jako znaczące.
Możesz tak ustawić, aby ścieżki kodu, do których nie przypisano zmiennej, generowały wyjątek (lub zwracały).
Jeśli planujesz wykonać jakąś akcję (np. Rejestrację) i ponownie rzucić wyjątek lub zgłosić inny wyjątek, a dzieje się to we wszystkich klauzulach catch, w których zmienna nie jest przypisana, kompilator będzie wiedział, że zmienna została przypisana:
To się kompiluje i może być rozsądnym wyborem. Jednak w rzeczywistej aplikacji, chyba że wyjątek zostanie zgłoszony tylko w sytuacjach, w których próba odzyskania * nie ma sensu , powinieneś upewnić się, że gdzieś go wychwytujesz i odpowiednio go obsługuje .
(W tej sytuacji nie można również odczytać zmiennej w bloku na końcu, ale nie wydaje się, że powinieneś być w stanie - w końcu bloki zasadniczo zawsze działają, aw tym przypadku zmienna nie zawsze jest przypisana .)
* Na przykład wiele aplikacji nie ma klauzuli catch, która obsługuje wyjątek OutOfMemoryException, ponieważ wszystko, co mogłyby z tym zrobić, może być co najmniej tak samo złe, jak awaria .
Może naprawdę nie chcą byłaby kod.
W swoim przykładzie wprowadzasz
firstVariable
isecondVariable
wypróbowujesz bloki. Jak już powiedziałem, możesz zdefiniować je przed blokami try, do których są przypisane, aby pozostały one w zasięgu, a także możesz zaspokoić / oszukać kompilator, umożliwiając czytanie z nich, upewniając się, że są one zawsze przypisane.Ale kod pojawiający się po tych blokach prawdopodobnie zależy od ich prawidłowego przypisania. W takim przypadku Twój kod powinien to odzwierciedlić i zapewnić.
Po pierwsze, czy (i powinienem) rzeczywiście poradzić sobie z błędem? Jednym z powodów, dla których istnieje obsługa wyjątków, jest ułatwienie radzenia sobie z błędami, w których można je skutecznie obsłużyć , nawet jeśli nie jest to blisko miejsca ich wystąpienia.
Jeśli nie jesteś w stanie obsłużyć błędu w funkcji, która została zainicjowana i używa tych zmiennych, być może blok try nie powinien w ogóle znajdować się w tej funkcji, ale powinien być gdzieś wyżej (tj. W kodzie wywołującym tę funkcję lub w kodzie który wywołuje ten kod). Tylko upewnij się, że nie przypadkowo złapałeś wyjątek zgłoszony w innym miejscu i błędnie zakładając, że został zgłoszony podczas inicjowania
firstVariable
isecondVariable
.Innym podejściem jest umieszczenie kodu używającego zmiennych w bloku try. Jest to często rozsądne. Ponownie, jeśli te same wyjątki, które wychwytujesz z ich inicjatorów, mogą być również wyrzucone z otaczającego kodu, powinieneś upewnić się, że nie zaniedbujesz tej możliwości podczas ich obsługi.
(Zakładam, że inicjujesz zmienne wyrażeniami bardziej skomplikowanymi niż te pokazane w twoich przykładach, tak, że mogą one generować wyjątek, a także, że tak naprawdę nie planujesz wychwycić wszystkich możliwych wyjątków , ale po prostu złapać jakieś wyjątki wyjątkowe możesz przewidzieć i w znaczący sposób obsłużyć . To prawda, że rzeczywisty świat nie zawsze jest taki fajny, a kod produkcyjny czasami to robi , ale ponieważ Twoim celem jest tutaj obsługa błędów, które występują podczas inicjowania dwóch określonych zmiennych, wszelkie klauzule catch, które piszesz dla tego konkretnego cel powinien być specyficzny dla wszelkich błędów).
Trzecim sposobem jest wyodrębnienie kodu, który może zawieść, oraz try-catch, który go obsługuje, we własnej metodzie. Jest to przydatne, jeśli najpierw chcesz całkowicie zająć się błędami, a następnie nie martwić się przypadkowym wyłapaniem wyjątku, który zamiast tego powinien zostać rozwiązany w innym miejscu.
Załóżmy na przykład, że chcesz natychmiast zamknąć aplikację po niepowodzeniu przypisania którejkolwiek ze zmiennych. (Oczywiście nie każda obsługa wyjątków dotyczy błędów krytycznych; jest to tylko przykład i może, ale nie musi, sposób, w jaki aplikacja ma reagować na problem). Możesz to zrobić w ten sposób:
Ten kod zwraca i dekonstruuje ValueTuple ze składnią C # 7.0, aby zwrócić wiele wartości, ale jeśli nadal korzystasz z wcześniejszej wersji C #, nadal możesz użyć tej techniki; na przykład można użyć parametrów out lub zwrócić niestandardowy obiekt, który udostępnia obie wartości . Ponadto, jeśli te dwie zmienne nie są ściśle ze sobą powiązane, prawdopodobnie lepiej byłoby mieć dwie osobne metody.
Zwłaszcza jeśli masz wiele takich metod, powinieneś rozważyć scentralizowanie kodu w celu powiadamiania użytkownika o poważnych błędach i rezygnacji. (Na przykład, można napisać
Die
metodę zmessage
parametrem.) Linia nie jest faktycznie wykonywana , więc nie trzeba (i nie powinien) napisać klauzulę catch dla niego.throw new InvalidOperationException();
Oprócz zamykania, gdy wystąpi konkretny błąd, możesz czasami napisać kod, który wygląda tak, jeśli zgłosisz wyjątek innego typu, który otacza oryginalny wyjątek . (W tej sytuacji, byś nie potrzebuje drugiego, nieosiągalny wyrażenie throw).
Wniosek: Zakres jest tylko częścią obrazu.
Możesz osiągnąć efekt owijania kodu z obsługą błędów bez refaktoryzacji (lub, jeśli wolisz, prawie bez refaktoryzacji), po prostu oddzielając deklaracje zmiennych od ich przypisań. Kompilator pozwala na to, jeśli spełniasz określone reguły przypisania w języku C #, a zadeklarowanie zmiennej przed blokiem try powoduje, że jej większy zakres jest jasny. Ale dalsze refaktoryzacja może być nadal najlepszą opcją.
źródło