Чому вислів "продовження" не може знаходитись у блоці "нарешті"?


107

У мене немає проблем; Мені просто цікаво. Уявіть такий сценарій:

foreach (var foo in list)
{
    try
    {
         //Some code
    }
    catch (Exception)
    {
        //Some more code
    }
    finally
    {
        continue;
    }
}

Це не компілюється, оскільки це призводить до помилки компілятора CS0157 :

Контроль не може залишити остаточне застереження

Чому?


7
Так. Просто цікаво. Якщо ви маєте повний сенс, чому він не збирається, чому ви хочете, щоб хтось пояснив, що вже має сенс? =)
Дж. Стін

14
чому б вам потрібно continue;в finallyблоці? Хіба це не так, як continue;після try -- catchблоку?
bansi

6
@ J.Steen Ні, я ВАМ знаю, що finally/continueце обмеження компілятора C # :-) Мені цікаво і причина цього обмеження.
xanatos

5
@xanatos - технічно кажучи, це обмеження основної CIL. З мовної специфікації : "Передача управління ніколи не дозволяється вводити обробник лову або остаточно клаузу, за винятком механізму обробки винятків." та "Передача контролю із захищеного регіону дозволена лише через інструкцію щодо винятку (вийти, end.filter, end.catch або end.finally)." brСімейство команд розгалуження не може досягти цього.
Непідписаний

1
@ Підписано: це також хороше обмеження, яке дозволяє йому бути химерним :)
user7116

Відповіді:


150

finallyблоки запускаються, викинутий виняток чи ні. Якщо викинутий виняток, що б чорт continueробив? Ви не можете продовжувати виконання циклу, оскільки нездійснене виключення передасть управління іншій функції.

Навіть якщо не буде винято жодного винятку, finallyвін запускається, коли інші оператори передачі управління всередині запуску блоку спробу / лову, як return, наприклад, викликає ту ж проблему.

Коротше кажучи, з семантикою finallyне має сенсу допускати передачу контролю зсередини finallyблоку на зовнішній його.

Підтримка цього за допомогою альтернативної семантики було б більш заплутаною, ніж корисною, оскільки існують прості шляхи вирішення, які роблять зрозуміліший спосіб поведінки. Тож ви отримуєте помилку і змушені правильно думати над своєю проблемою. Це загальна ідея "кинути тебе в яму успіху", яка продовжується в C #.

C #, ти, і вихід, якщо успіх

Якщо ви хочете ігнорувати винятки (частіше за все це погана ідея) і продовжувати виконувати цикл, використовуйте блок catch all:

foreach ( var in list )
{
    try{
        //some code
    }catch{
        continue;
    }
}

Якщо ви хочете continueлише тоді, коли не викидаються вилучені винятки, просто поставте continueза межі пробного блоку.


12
Я прийму це як відповідь, оскільки ви розумієте причини, чому Microsoft, можливо, вирішила остаточно не прийняти продовження. Напевно, що образи переконали і мене :)
lpaloub

20
чи можете ви розробити ідею "кинути вас у яму успіху"? Я цього не зрозумів :-D
Ant Ant


13
Зображення зробив Джон Скіт, btw. Я дістався звідси: msmvps.com/blogs/jon_skeet/archive/2010/09/02/…
R. Martinho Fernandes

1
Звичайно, a continueв finallyблоці буде нормально, якщо він продовжує лише локальний цикл, визначений всередині finallyблоку. У питанні, однак, він намагається продовжити "зовнішню" петлю. Подібні заяви , що ви не можете мати всередині finallyблоку знаходяться return, break(при дробленні з блоку) і goto(при переході до мітки за межами finallyблоку). Для пов'язаної дискусії на Java див. Повернення з остаточного блоку на Java .
Jeppe Stig Nielsen

32

Ось надійне джерело:

Оператор продовження не може вийти з остаточно блоку (Розділ 8.10). Коли оператор продовження відбувається в остаточному блоці, ціль оператора продовження повинна знаходитися в межах одного остаточного блоку; в іншому випадку виникає помилка часу компіляції.

Це взято з MSDN, 8.9.2. Продовження .

У документації сказано, що:

Висловлювання остаточно блоку завжди виконуються, коли контроль залишає спробу оператора. Це вірно, чи передача керування відбувається в результаті звичайного виконання, в результаті виконання перерви, продовження, переходу або повернення, або в результаті розповсюдження виключення з оператора спробу. Якщо виняток буде кинуто під час виконання остаточного блоку, виняток поширюється на наступне додане спробу оператора. Якщо інший виняток був у процесі поширення, цей виняток втрачається. Процес розповсюдження винятку обговорюється далі в описі заяви викидання (Розділ 8.9.5).

Це звідси 8.10 Спроба твердження .


31

Ви можете подумати, що це має сенс, але насправді це не має сенсу .

foreach (var v in List)
{
    try
    {
        //Some code
    }
    catch (Exception)
    {
        //Some more code
        break; or return;
    }
    finally
    {
        continue;
    }
}

Що ви маєте намір зробити перерву чи продовжити, коли буде викинуто виняток? Команда компілятора C # не хоче приймати рішення самостійно, вважаючи, що breakабо continue. Натомість вони вирішили поскаржитися на ситуацію з розробником, що буде передавати контроль із неоднозначного становища finally block.

Тому завдання розробника чітко вказати, що він має намір робити, а не компілятор припускати щось інше.

Я сподіваюся, ви зрозуміли, чому це не складається!


У відповідній примітці, навіть якщо не було "улову", на шлях виконання після finallyзаяви слід впливати на те, чи catchвийшов виняток через виняток, і немає механізму сказати, як це має взаємодіяти з a continue. Мені подобається ваш приклад, тому що він показує ще більшу проблему.
supercat

@supercat Я погоджуюся з вами, моя відповідь показує приклад неоднозначної ситуації для компілятора, але з таким підходом існує так багато проблем.
Sriram Sakthivel

16

Як заявили інші, але зосереджені на винятках, мова йде дійсно про неоднозначне поводження з передачею контролю.

На вашу думку, ви, мабуть, думаєте про такий сценарій:

public static object SafeMethod()
{
    foreach(var item in list)
    {
        try
        {
            try
            {
                //do something that won't transfer control outside
            }
            catch
            {
                //catch everything to not throw exceptions
            }
        }
        finally
        {
            if (someCondition)
                //no exception will be thrown, 
                //so theoretically this could work
                continue;
        }
    }

    return someValue;
}

Теоретично ви можете відстежувати контрольний потік і сказати, так, це "нормально". Не виключається виняток, контроль не передається. Але дизайнери мови C # мали на увазі інші проблеми.

Виняток з кидання

public static void Exception()
{
    try
    {
        foreach(var item in list)
        {
            try
            {
                throw new Exception("What now?");
            }
            finally
            {
                continue;
            }
        }
    }
    catch
    {
        //do I get hit?
    }
}

Страшний Гото

public static void Goto()
{
    foreach(var item in list)
    {
        try
        {
            goto pigsfly;
        }
        finally
        {
            continue;
        }
    }

    pigsfly:
}

Повернення

public static object ReturnSomething()
{
    foreach(var item in list)
    {
        try
        {
            return item;
        }
        finally
        {
            continue;
        }
    }
}

Поломка

public static void Break()
{
    foreach(var item in list)
    {
        try
        {
            break;
        }
        finally
        {
            continue;
        }
    }
}

Отже , у висновку, та, хоча це невелика можливість використання continueв ситуаціях , коли управління не передаються, але багато (більшість?) Випадків включають виняток або returnблоки. Мовні дизайнери вважають, що це буде занадто неоднозначно і (ймовірно) неможливо забезпечити під час компіляції, що ваш continueвикористовується лише у випадках, коли потік управління не передається.


Отримала таку ж ситуацію в Яві, коли Proguard розбився під час затуманення. Ваше пояснення досить добре. C # дає помилку часу компіляції - ідеально має сенс.
Dev_Vikram

11

Взагалі continueне має сенсу при використанні в finallyблоці. Погляньте на це:

foreach (var item in list)
{
    try
    {
        throw new Exception();
    }
    finally{
        //doesn't make sense as we are after exception
        continue;
    }
}

"Взагалі" багато речей не має сенсу. І це не означає, що він не повинен працювати «зокрема». IE: continueне має сенсу зовнішній вислів циклу, але це не означає, що він не підтримується.
zerkms

Я думаю, це має певний сенс. itemможе бути файл, читання не вдалося -> finallyзакриває файл. continueзапобігає решту обробки.
jnovacho

5

"Це не компілюється, і я думаю, що це має повний сенс"

Ну, я думаю, це не так.

Коли ви буквально є, catch(Exception)то вам нарешті не потрібен (і, мабуть, навіть не той continue).

Якщо у вас є більш реалістичний catch(SomeException), що має статися, коли виняток не буде спійманий? Ви continueхочете піти в один бік, виняток - керувати іншим.


2
Я думаю, що ти можеш потребувати, finallyколи маєш catch. Він зазвичай використовується для надійного закриття ресурсів.
jnovacho

Це не лише винятки. Ви можете легко мати returnвиписку в межах вашого tryабо catchблоку. Що ми робимо зараз? Повернути чи продовжити цикл? (і якщо ми продовжимо, що робити, якщо це останній елемент для повторення? Тоді ми просто продовжуємо виходити за межі, foreachа повернення ніколи не відбувається?) EDIT: Або навіть gotoдо мітки поза циклом, майже будь-якої дії, яка передає керування поза foreachциклом .
Кріс Сінклер

2
Я не згоден з "Коли ви буквально маєте улов (Виняток), вам нарешті не потрібен (і, мабуть, навіть не продовження)." Що робити, якщо мені потрібно зробити операцію незалежно від того, зробив чи ні виняток?
lpaloub

Гаразд, нарешті обслуговує додаткові returns всередині блоку. Але, як правило, виконання продовжується після загальної кількості.
Хенк Холтерман

"нарешті" - це не "зловити". Його слід використовувати для коду очищення. нарешті блок працює незалежно від того, кинуто виняток чи ні . Такі речі, як закриття файлів або звільнення пам’яті (якщо ви використовуєте некерований код) і т. Д. Це те, що ви ставите в остаточному блоці
Robotnik

3

Ви не можете залишити корпус остаточно блоку. Це включає перерву, повернення і у вашому випадку продовжуйте ключові слова.


3

finallyБлок може бути виконаний з виключенням чекати , щоб бути викликані повторно. Насправді не було б сенсу мати можливість вийти з блоку ( continueабо іншим), не скидаючи виняток.

Якщо ви хочете продовжувати цикл, що б не трапилося, вам не потрібно остаточного твердження: Просто вловіть виняток і не перекидайте.


1

finallyзапускається, чи не кидається невиконаний виняток. Інші вже пояснили, чому це стає continueнелогічним, але ось альтернатива, яка відповідає духу того, про що цей код, як видається, вимагає. В основному, finally { continue; }говорить:

  1. Коли трапляються винятки, продовжуйте
  2. Коли є виняткові випади, дозвольте їх кинути, але все одно продовжуйте

(1) можна задовольнити, розміщуючи continueв кінці кожного catch, і (2) можна задовольнити, зберігаючи вилучені винятки, які будуть кинуті пізніше. Ви можете написати це так:

var exceptions = new List<Exception>();
foreach (var foo in list) {
    try {
        // some code
    } catch (InvalidOperationException ex) {
        // handle specific exception
        continue;
    } catch (Exception ex) {
        exceptions.Add(ex);
        continue;
    }
    // some more code
}
if (exceptions.Any()) {
    throw new AggregateException(exceptions);
}

Власне, finallyстратили б і в третьому випадку, коли не було жодних винятків, кинутих, спійманих чи не злових. Якщо цього було бажано, ви можете, звичайно, просто розмістити сингл continueпісля блоку спробу лову, а не всередині кожного catch.


1

Технічно кажучи, це обмеження основної CIL. З мовної специфікації :

Передача управління ніколи не дозволяється вводити обробник лову або остаточно ставитись за винятком випадків, за допомогою механізму обробки винятків.

і

Передача управління з захищеної області дозволяється тільки через інструкцію виключення ( leave, end.filter, end.catchабо end.finally)

На сторінці документа для brвказівки :

Ця інструкція не може виконувати передачу елементів керування в і випробовувати, ловити, фільтрувати та, нарешті, блоки.

Останнє справедливо для всіх галузевих інструкцій, в тому числі beq, brfalseі т.д.


-1

Дизайнери мови просто не хотіли (або не могли) міркувати про семантику остаточного блоку, що закінчується передачею управління.

Одне питання, чи, можливо, ключове, полягає в тому, що finallyблок виконується як частина передачі нелокального контролю (обробка виключень). Ціль передачі управління - це не цикл, що охоплює; обробка виключення скасовує цикл і продовжує розкручуватися далі.

Якщо у нас передача керування вийшла з finallyблоку очищення, то початкова передача управління "викрадена". Він скасовується, а контроль йде в іншому місці.

Семантику можна опрацювати. Інші мови мають його.

Дизайнери C # вирішили просто заборонити статичні "готоподібні" передачі управління, тим самим дещо спростивши речі.

Однак, навіть якщо ви це зробите, це не вирішує питання про те, що відбувається, якщо динамічна передача ініціюється з а finally: що робити, якщо нарешті блок викликає функцію, і ця функція кидає? Оригінальну обробку винятків потім "викрадено".

Якщо ви відпрацьовуєте семантику цієї другої форми викрадення, немає ніяких причин виганяти перший тип. Вони насправді те саме: передача управління - це передача керування, незалежно від того, лексична вона чи ні.


Різниця полягає в тому, що finallyза визначенням слід негайно супроводжувати продовження передачі, яка його ініціювала (невдале виключення returnтощо). Було б легко дозволити "готоподібні" передачі всередині finally(які мови це роблять, до речі?), Але це може означати, що це try { return } finally { ... } може не повернутися , що є абсолютно несподіваним. Якщо finallyвикликає функцію, яка кидає, це насправді не те саме, тому що ми вже очікуємо, що винятки можуть виникнути в будь-який час і перервати нормальний потік.
nmclean

@nmclean: Насправді виняток, який уникає, finallyє щось подібне, оскільки це може спричинити несподіваний та нелогічний потік програми. Єдина відмінність полягає в тому, що мовні дизайнери, не маючи змоги відхилити всі програми, де нарешті блок міг би викинути необроблений виняток, натомість дозволяють таким програмам компілюватись і сподіваються, що програма може прийняти будь-які наслідки, які можуть бути наслідком відмови від попереднього відключення виключення послідовність.
supercat

@supercat Я погоджуюся з тим, що він порушує очікуваний потік, але мій погляд на те, що це завжди стосується необроблених винятків, незалежно від того, поточний потік - це finallyчи ні. За логікою останнього абзацу Kaz, функції також повинні вільно впливати на потік в області дії абонента continueтощо, оскільки їм це дозволяється за винятком. Але це, звичайно, не дозволено; винятки є єдиним винятком із цього правила, звідси назва "виняток" (або "переривання").
nmclean

@nmclean: Невкладені винятки являють собою контрольний потік, який відрізняється від звичайного виконання, але все ще структурований. Оператор, що слідує за блоком, повинен виконуватися лише в тому випадку, якщо все винятки, що мали місце в цьому блоці, потрапили в нього. Це може здатися розумним очікуванням, але виняток, який трапляється всередині finallyблоку, може порушити це. Дійсно неприємна ситуація, яку IMHO слід було б вирішити, як мінімум повідомити finallyблоки про будь-які очікувані винятки, щоб вони могли будувати складені об'єкти виключень.
supercat

@mmclean Вибачте, я не згоден, що назва "виняток" походить від якогось "виключення з правила" (щодо контролю), характерного для C #, LOL. Аспект зміни потоку управління винятком є ​​просто не локальним динамічним передачею управління. Регулярний контроль передачі в межах тієї ж лексичної сфери (не відбувається розкручування) може розглядатися як особливий випадок цього.
Каз
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.