Odkładając na bok spójność, czy nie ma sensu pakować naszego kodu w obsługę błędów bez konieczności refaktoryzacji?
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:
var zerothVariable = 1_000_000_000_000L;
int firstVariable;
try {
// Change checked to unchecked to allow the overflow without throwing.
firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
}
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:
Console.WriteLine(firstVariable);
To da błąd podczas kompilacji :
CS0165 Zastosowanie nieprzypisanej zmiennej lokalnej „firstVariable”
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ć:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
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ć, że n
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:
var n = 0; // But is this meaningful, or just covering a bug?
try {
n = 10;
}
catch (IOException) {
}
Console.WriteLine(n);
Lub:
int n;
try {
n = 10;
}
catch (IOException) {
n = 0; // But is this meaningful, or just covering a bug?
}
Console.WriteLine(n);
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:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
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
i secondVariable
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
i secondVariable
.
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:
// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
try {
// This code is contrived. The idea here is that obtaining the values
// could actually fail, and throw a SomeSpecificException.
var firstVariable = 1;
var secondVariable = firstVariable;
return (firstVariable, secondVariable);
}
catch (SomeSpecificException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
throw new InvalidOperationException(); // unreachable
}
}
// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
var (firstVariable, secondVariable) = GetFirstAndSecondValues();
// Code that does something with them...
}
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ę z message
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ą.
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.