Чому в C #, чому змінні оголошуються всередині пробного блоку обмежені в області дії?


23

Я хочу додати обробку помилок до:

var firstVariable = 1;
var secondVariable = firstVariable;

Нижче не компілюється:

try
{
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Чому необхідно, щоб блок спробу вловлювання впливав на область змінних, як це роблять інші блоки коду? Чи не було б сенсу для нас, щоб ми могли обернути наш код обробкою помилок, не потребуючи рефактора?


14
A try.. catch- це специфічний тип кодового блоку, і що стосується всіх блоків коду, ви не можете оголосити змінну в одній і використовувати ту саму змінну в іншій як питання сфери.
Ніл

"- це специфічний тип кодового блоку". Конкретним, яким способом? Дякую
JᴀʏMᴇᴇ

7
Я маю на увазі що-небудь між фігурними дужками - це код коду. Ви бачите, як слід після оператора if і за твердженням for, хоча концепція однакова. Вміст знаходиться в підвищеному обсязі щодо його батьківського обсягу. Я впевнений, що це буде проблематично, якби ви просто використовували фігурні дужки {}без спроб.
Ніл

Порада: Зауважте, що використання (IDisposable) {} і просто {} застосовується також аналогічно. Якщо ви використовуєте використання з IDisposable, воно автоматично очищатиме ресурси незалежно від успіху чи невдачі. Є кілька винятків із цього, як і не всі класи, які ви очікували б впровадити IDisposable ...
Джулія МакГуйган

1
Багато дискусії з цього ж питання на StackOverflow, тут: stackoverflow.com/questions/94977 / ...
Джон Шнайдер

Відповіді:


90

Що робити, якщо ваш код:

try
{
   MethodThatMightThrow();
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Тепер ви намагаєтеся використовувати незадекларовану змінну ( firstVariable), якщо ваш виклик методу кидає.

Примітка . Вищенаведений приклад конкретно відповідає на початкове запитання, в якому йдеться про "узгодженість ради". Це свідчить про наявність інших причин, крім послідовності. Але, як показує відповідь Петра, є також вагомий аргумент від послідовності, який, безумовно, був би дуже важливим фактором рішення.


Ага, це саме те, що я був після. Я знав, що є деякі мовні особливості, які роблять те, що я пропоную неможливим, але я не міг придумати жодного сценарію. Дуже дякую.
JᴀʏMᴇᴇ

1
"Тепер ви б намагалися використовувати незадекларовану змінну, якщо ваш виклик методу кидає." Крім того, припустимо, цього уникнути, обробляючи змінну так, ніби вона була оголошена, але не ініціалізована перед кодом, який може кинути. Тоді це не буде оголошено незадекларованим, але воно все ще буде потенційно непризначеним, а визначений аналіз призначення забороняє читати його значення (без втручається завдання, яке може виявитись).
Елія Каган

3
Для статичної мови на зразок C #, декларування дійсно актуально лише під час компіляції. Компілятор може легко перемістити декларацію раніше в області застосування. Більш важливим фактом під час виконання є те, що змінна може бути ініціалізована .
jpmc26

3
Я не згоден з цією відповіддю. У C # вже є правило, з якого неінциніалізовані змінні неможливо прочитати з деяким обізнаністю про потік даних. (Спробуйте оголосити змінні у випадках а switchта отримати доступ до них в інших.) Це правило може легко застосувати тут і запобігти компіляції цього колу в будь-якому випадку. Я думаю, що відповідь Петра нижче є більш правдоподібною.
Себастьян Редл

2
Існує різниця між незадекларованими неініціалізованими та C # відстежує їх окремо. Якщо вам дозволили використовувати змінну за межами блоку, де вона була оголошена, це означатиме, що ви зможете призначити її в першому catchблоці, і тоді вона буде визначена в другому tryблоці.
svick

64

Я знаю, що на це добре відповів Бен, але я хотів звернутися до послідовності POV, яка була зручно відсунута вбік. Якщо припустити, що try/catchблоки не впливають на сферу застосування, то ви закінчите:

{
    // new scope here
}

try
{
   // Not new scope
}

І для мене це врізається головою в Принцип найменшого здивування (POLA), тому що тепер у вас є {і }виконуються подвійні обов'язки залежно від контексту того, що їм передувало.

Єдиний вихід з цього безладу - це позначити якийсь інший маркер для розмежування try/catchблоків. Який починає додавати кодовий запах. Тож, доки ви не володієте необмеженою try/catchмовою, це було б таким безладом, що вам було б краще з широкомасштабною версією.


Ще одна відмінна відповідь. І я ніколи не чув про POLA, так приємно подальше читання. Велике спасибі товаришу.
JᴀʏMᴇᴇ

"Єдиний вихід із цього безладу - це позначити якийсь інший маркер для розмежування try/ catchблокування." - ти маєш на увазі try { { // scope } }:? :)
CompuChip

@CompuChip, який би мав {}витягнути подвійне мито як сферу застосування, а не створювати сферу застосування залежно від контексту try^ //no-scope ^буде прикладом іншого маркера.
Лелієль

1
На мою думку, це набагато більш фундаментальна причина і ближче до "реальної" відповіді.
Джек Едлі

@JackAidley тим більше, що ви вже можете писати код там, де ви використовуєте змінну, яка може не бути призначена. Тож як відповідь Бена має сенс про те, наскільки це корисна поведінка, я не вважаю це причиною такої поведінки. У відповіді Бена зазначається, що ОП говорить: "послідовність ради в сторону", але послідовність - цілком прекрасна причина! Вузька область застосування має всілякі інші переваги.
Кет

21

Чи не було б сенсу для нас, щоб ми могли обернути наш код обробкою помилок, не потребуючи рефактора?

Щоб відповісти на це, потрібно розглянути більше, ніж просто область змінної .

Навіть якби змінна залишалася в області застосування, вона не була б точно визначена .

Оголошення змінної у блоці спробу висловлює - компілятору та людським читачам - що вона має сенс лише у цьому блоці. Компілятору корисно виконати це.

Якщо ви хочете, щоб змінна була в області дії після блоку спробу, ви можете оголосити її поза блоком:

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 #, а оголошення оголошень змінної перед блоком спробу дає зрозуміти її більшу область застосування. Але рефакторинг далі може бути найкращим варіантом.


"коли ви пишете код у блок спробу з клаузами catch, ви говорите як компілятору, так і будь-яким читачам людей, що до нього слід ставитися так, як це може бути не в змозі запуститися." Компілятор переймається тим, що контроль може дійти до більш пізніх висловлювань, навіть якщо попередні твердження становлять виняток. Компілятор зазвичай передбачає, що якщо одне твердження видає виняток, наступне твердження не буде виконано, тому непризначена змінна не буде прочитана. Додавання "catch" дозволить контролю надходити до більш пізніх висловлювань - важливий саме улов, а не те, чи кидає код у спробу блоку.
Піт Кіркхем
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.