Чи не було б сенсу для нас, щоб ми могли обернути наш код обробкою помилок, не потребуючи рефактора?
Щоб відповісти на це, потрібно розглянути більше, ніж просто область змінної .
Навіть якби змінна залишалася в області застосування, вона не була б точно визначена .
Оголошення змінної у блоці спробу висловлює - компілятору та людським читачам - що вона має сенс лише у цьому блоці. Компілятору корисно виконати це.
Якщо ви хочете, щоб змінна була в області дії після блоку спробу, ви можете оголосити її поза блоком:
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);
}
Це виражає, що змінна може мати значення поза блоком спробу. Компілятор це дозволить.
Але це також показує іншу причину, за якою зазвичай не було б корисно зберігати змінні в області після введення їх у блок спробу. Компілятор C # виконує визначений аналіз призначення та забороняє зчитувати значення змінної, для якої вона не була доведена, їй було надано значення.Тож ви все ще не можете прочитати зі змінної.
Припустимо, я намагаюся прочитати зі змінної після блоку спробу:
Console.WriteLine(firstVariable);
Це дасть помилку часу компіляції :
CS0165 Використання непризначеної локальної змінної 'firstVariable'
Я назвав Environment.Exit в блоці зловити, так що я знаю , що змінна була призначена до виклику Console.WriteLine. Але компілятор цього не підводить.
Чому компілятор такий суворий?
Я навіть не можу цього зробити:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
Один із способів розглянути це обмеження - сказати, що визначений аналіз призначення в C # не дуже складний. Але ще один спосіб подивитися на це - коли ви пишете код у блок спробу з клавішами catch, ви говорите як компілятору, так і будь-яким людським читачам, що до нього слід ставитися так, як це може бути не в змозі запуститись.
Щоб проілюструвати, що я маю на увазі, уявіть, якби компілятор дозволив код вище, але потім ви додали виклик у блок спробу до функції, про яку ви особисто знаєте, не викине винятку . Не маючи змоги гарантувати, що викликана функція не кинула an IOException
, компілятор не міг знати, що n
призначено, і тоді вам доведеться робити рефактор.
Це означає, що, виходячи з дуже складного аналізу для визначення того, чи змінна, присвоєна блоку спробу з клавішами catch, після цього визначено призначена, компілятор допомагає вам уникнути написання коду, який, швидше за все, зламається пізніше. (Зрештою, ловити виняток зазвичай означає, що ви думаєте, що вас можуть кинути.)
Ви можете переконатися, що змінна призначена через усі шляхи коду.
Ви можете зробити компіляцію коду, надавши змінній значення перед блоком спробу або в блоці вилову. Таким чином, він все ще буде ініціалізований або призначений, навіть якщо призначення в блоці спробу не відбудеться. Наприклад:
var n = 0; // But is this meaningful, or just covering a bug?
try {
n = 10;
}
catch (IOException) {
}
Console.WriteLine(n);
Або:
int n;
try {
n = 10;
}
catch (IOException) {
n = 0; // But is this meaningful, or just covering a bug?
}
Console.WriteLine(n);
Ці компілюють. Але найкраще робити лише щось подібне, якщо вказане значення за замовчуванням має сенс * та виробляє правильну поведінку.
Зауважте, що у цьому другому випадку, коли ви призначаєте змінну у блоці спробу та у всіх блоках лову, хоча ви зможете прочитати змінну після пробного вилову, ви все одно не зможете прочитати змінну всередині доданого finally
блоку , оскільки виконання може залишити пробний блок у більшій кількості ситуацій, ніж ми часто думаємо .
* До речі, деякі мови, такі як C і C ++, обидві дозволяють неініціалізовані змінні і не мають певного аналізу призначення, щоб запобігти читанню з них. Оскільки читання неініціалізованої пам’яті спричиняє поведінку програм недетермінованим та нестабільним способом, зазвичай рекомендується уникати введення змінних на цих мовах без надання ініціалізатора. У мовах з певним аналізом призначення, як C # та Java, компілятор врятує вас від читання неініціалізованих змінних, а також від меншого зла ініціалізації їх безглуздими значеннями, які згодом можуть неправильно трактуватися як значущі.
Ви можете зробити так, щоб кодові шляхи, яким змінна не призначена, кидали виняток (або повертають).
Якщо ви плануєте виконати якусь дію (наприклад, ведення журналу) та повторно скинути виняток або викинути інший виняток, і це відбувається в будь-яких застереженнях вилову, де змінна не призначена, тоді компілятор буде знати, що змінна була призначена:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
Це компілює і може бути розумним вибором. Однак у фактичному застосуванні, якщо виняток не підпадає лише у ситуаціях, коли навіть не має сенсу намагатися відновити * , слід переконатися, що ви все-таки десь ловите та належним чином обробляєте його .
(Ви також не можете прочитати змінну в остаточному блоці в цій ситуації, але це не здається, що ви повинні вміти - зрештою, блоки, по суті, завжди працюють, і в цьому випадку змінна не завжди призначається .)
* Наприклад, у багатьох додатках немає положення про вилов, який обробляє OutOfMemoryException, оскільки все, що вони могли зробити з цим, може бути як мінімум таким же поганим, як і збої .
Може бути , ви на самому справі дійсно хочете , щоб реорганізувати код.
У своєму прикладі ви вводите firstVariable
і secondVariable
пробні блоки. Як я вже говорив, ви можете визначити їх перед тим, як спробувати блоки, в які вони призначені, щоб вони залишалися в обсягах згодом, і ви можете задовольнити / ввести компілятора, щоб він міг читати з них, переконуючись, що вони завжди призначені.
Але код, який з’являється після цих блоків, мабуть, залежить від того, чи правильно вони були призначені. Якщо це так, то ваш код повинен відображати та забезпечувати це.
По-перше, чи можете (і повинні) ви насправді впоратися з помилкою? Однією з причин поводження з винятками є спрощення керування помилками, коли з ними можна ефективно працювати , навіть якщо це не поруч, де вони трапляються.
Якщо ви фактично не можете впоратися з помилкою у функції, яка ініціалізується та використовує ці змінні, можливо, блок спробу не повинен бути в цій функції взагалі, а натомість десь вище (тобто в коді, який викликає цю функцію, або код що викликає цей код). Просто переконайтеся, що ви випадково не потрапили до винятку, кинутого кудись і неправильно припускаючи, що він був кинутий під час ініціалізації firstVariable
та secondVariable
.
Інший підхід полягає в тому, щоб помістити код, який використовує змінні, у блок спробу. Це часто розумно. Знову ж таки, якщо ті самі винятки, які ви ловите з їхніх ініціалізаторів, також можуть бути викинуті з навколишнього коду, ви повинні переконатися, що ви не нехтуєте цією можливістю, обробляючи їх.
(Я припускаю, що ви ініціалізуєте змінні з виразами складнішими, ніж показано у ваших прикладах, таким чином, що вони насправді можуть викинути виняток, а також, що ви насправді не плануєте ловити всі можливі винятки , а просто ловити будь-які конкретні винятки Ви можете передбачити і змістовно обробити . Це правда, що реальний світ не завжди такий приємний, а виробничий код іноді це робить , але оскільки ваша мета тут - обробляти помилки, які виникають під час ініціалізації двох конкретних змінних, будь-які зауваження, що ви записуєте для цього конкретного мета повинна бути специфічною для будь-яких помилок.)
Третій спосіб - витягнути код, який може вийти з ладу, і пробний лов, який його обробляє, у свій власний метод. Це корисно, якщо ви хочете спочатку вирішити помилки повністю, а потім не турбуватися про те, щоб ненавмисно зловити виняток, який слід обробляти десь в іншому місці.
Припустимо, наприклад, що ви хочете негайно вийти з програми після відмови призначити будь-яку змінну. (Очевидно, що не все обробляння винятків стосується фатальних помилок; це лише приклад, і може бути, а може і не так, як ви хочете, щоб ваша програма реагувала на проблему.) Ви можете зробити щось подібне:
// 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...
}
Цей код повертає та деконструює ValueTuple з синтаксисом C # 7.0 для повернення кількох значень, але якщо ви все ще використовуєте більш ранню версію C #, ви все одно можете використовувати цю техніку; наприклад, ви можете використовувати параметри або повернути спеціальний об'єкт, який забезпечує обидва значення . Крім того, якщо дві змінні насправді не тісно пов'язані між собою, то, мабуть, краще все-таки мати два окремі методи.
Особливо, якщо у вас є кілька подібних методів, вам слід розглянути можливість централізації коду для повідомлення користувача про фатальні помилки та вихід з нього. (Наприклад, ви могли б написати Die
метод з message
параметром.) Лінія фактично ніколи не виконуються , так що ви не потрібні (і не повинна) написати статтю на вилов для нього.throw new InvalidOperationException();
Окрім відмови від виходу, коли виникає певна помилка, іноді ви можете написати код, який виглядає приблизно так, якщо ви кинете виняток іншого типу, який завершує початкове виключення . (У цій ситуації вам не знадобиться другий недоступний вираз кидка.)
Висновок: Область застосування - лише частина картини.
Ви можете домогтися ефекту перетворення коду за допомогою помилок без рефакторингу (або, якщо вам зручніше, майже без рефакторингу), просто відокремивши декларації змінних від їх призначення. Компілятор дозволяє це робити, якщо ви задовольняєте певні правила призначення C #, а оголошення оголошень змінної перед блоком спробу дає зрозуміти її більшу область застосування. Але рефакторинг далі може бути найкращим варіантом.
try.. catch
- це специфічний тип кодового блоку, і що стосується всіх блоків коду, ви не можете оголосити змінну в одній і використовувати ту саму змінну в іншій як питання сфери.