Чому повернення yield не може з’явитися всередині блоку try з catch?


95

Це нормально:

try
{
    Console.WriteLine("Before");

    yield return 1;

    Console.WriteLine("After");
}
finally
{
    Console.WriteLine("Done");
}

finallyБлок працює , коли все , що закінчив виконання ( IEnumerator<T>опори , IDisposableщоб забезпечити спосіб забезпечити це навіть тоді , коли перерахування припиняється до його завершення).

Але це не гаразд:

try
{
    Console.WriteLine("Before");

    yield return 1;  // error CS1626: Cannot yield a value in the body of a try block with a catch clause

    Console.WriteLine("After");
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Припустимо (заради аргументу), що виключення викликає той чи інший із WriteLineвикликів усередині блоку try. У чому проблема продовження виконання в catchблоці?

Звичайно, частина повернення доходу (в даний час) не може кинути нічого, але чому це повинно заважати нам мати обгороджувальний файл try/ catchмати справу з винятками, викинутими до або після a yield return?

Оновлення: Тут є цікавий коментар Еріка Ліпперта - здається, у них вже є достатньо проблем із правильною поведінкою try / нарешті!

РЕДАГУВАТИ: Сторінка MSDN з цією помилкою: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Однак це не пояснює, чому.


2
Пряме посилання на коментар Еріка Ліпперта
Роман Старков

примітка: ви не можете поступитися і в самому блоці catch :-(
Simon_Weaver

2
Старе нове посилання більше не працює.
Себастьян Редл,

Відповіді:


50

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

Є кілька подібних речей, з якими я вже стикався:

  • Атрибути не можуть бути загальними
  • Неможливість X отримати похід від XY (вкладений клас у X)
  • Блоки ітератора використовують відкриті поля в сформованих класах

У кожному з цих випадків можна було б отримати трохи більше свободи, ціною додаткової складності в компіляторі. Команда зробила прагматичний вибір, за що я їм аплодую - я волів би трохи більш обмежувальну мову з 99,9% точним компілятором (так, є помилки; я натрапив на одного на SO буквально днями), ніж більше гнучка мова, яка не може правильно скомпілювати.

EDIT: Ось псевдодоказ того, як це можливо, чому це можливо.

Враховуйте, що:

  • Ви можете переконатися, що частина повернення yield не видає винятку (попередньо обчисліть значення, а потім ви просто встановите поле і повернете "true")
  • Вам дозволено try / catch, який не використовує return return у блоці ітератора.
  • Усі локальні змінні в блоці ітераторів є змінними екземпляра в згенерованому типі, тому ви можете вільно переміщувати код до нових методів

Тепер перетворимо:

try
{
    Console.WriteLine("a");
    yield return 10;
    Console.WriteLine("b");
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

into (сорт псевдокоду):

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    __current = 10;
    return true;

case just_after_yield_return:
    try
    {
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        CatchBlock();
    }
    goto case post;

case post;
    Console.WriteLine("Post");


void CatchBlock()
{
    Console.WriteLine("Catch block");
}

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

Можливо, я щось тут пропустив - якщо так, будь ласка, дайте мені знати!


11
Хороший доказ концепції, але ця стратегія стає болючою (можливо, більше для програміста на C #, ніж для програміста для компілятора на C #), коли ви починаєте створювати сфери з такими речами, як usingі foreach. Наприклад:try{foreach (string s in c){yield return s;}}catch(Exception){}
Брайан

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

7
"Я підозрюю, що є дуже, дуже мало випадків, коли це обмеження насправді є проблемою, з якою неможливо обійтись". Це як би сказати, що вам не потрібні винятки, оскільки ви можете використовувати стратегію повернення коду помилки, яка зазвичай використовується в C, тому багато років назад. Я визнаю, що технічні труднощі можуть бути значними, але це все одно суттєво обмежує корисність yield, на мій погляд, через коду спагеті, який потрібно написати, щоб обійти його.
jpmc26

@ jpmc26: Ні, це насправді зовсім не схоже на те, щоб це говорити. Я не пам’ятаю, щоб це колись мене кусало, і я багато разів використовував блоки ітераторів. Це трохи обмежує корисність yieldІМО - це далеко далеко від суворості .
Jon Skeet

2
Це «особливість» на насправді вимагає досить потворною коди в деяких випадках вирішити цю проблему, див stackoverflow.com/questions/5067188 / ...
namey

5

Усі yieldоператори у визначенні ітератора перетворюються у стан у машині стану, який ефективно використовує switchоператор для просування станів. Якщо він зробив генерувати код для yieldоператорів в Try / улов довелося б дублювати всі в tryблоці для кожного yield заяви, виключаючи всі інші yieldзаяви для цього блоку. Це не завжди можливо, особливо якщо одне yieldтвердження залежить від попереднього.


2
Я не думаю, що купую це. Я думаю, це було б цілком здійсненно - але дуже складно.
Джон Скіт,

2
Блоки try / catch у C # не призначені для повторного вступу. Якщо ви розділите їх, ви можете зателефонувати MoveNext () після винятку і продовжити блок спробу з можливим недійсним станом.
Марк Сідаде

2

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

Щоб отримати уявлення про те, про що я говорю, встановіть блок ітераторів та foreach за допомогою цього ітератора. Перевірте, як виглядає стек викликів усередині блоку foreach, а потім перевірте його всередині блоку try / нарешті ітератора.


Я знайомий з розгортанням стеку в C ++, де деструктори викликаються на локальних об'єктах, які виходять за межі обсягу. Відповідна річ у C # буде спроба / нарешті. Але це розмотування не відбувається, коли відбувається повернення врожаю. А для спроби / лову немає необхідності взаємодіяти з віддачею доходу.
Даніель Ервікер

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

@ Radu094: Ні, я впевнений, що це було б можливо. Не забувайте, що це вже обробляє остаточно, що принаймні дещо схоже.
Джон Скіт,

2

Я прийняв відповідь НЕЗБЕРЕЖНОГО КИСЛУ, поки не прийде хтось із Microsoft, щоб облити ідею холодною водою. Але я не згоден з частиною, що стосується думки - звичайно, правильний компілятор важливіший за повний, але компілятор C # вже дуже розумно розбирає цю трансформацію для нас, наскільки це можливо. Трохи більша повнота у цьому випадку полегшить користування мовою, її викладання, пояснення, з меншою кількістю крайових випадків чи проблем. Тож думаю, що варто було б додаткових зусиль. Кілька хлопців у Редмонді дряпають голови протягом двох тижнів, і в результаті мільйони кодерів протягом наступного десятиліття можуть трохи більше відпочити.

(Я також виховую жалюгідне бажання, щоб був спосіб зробити yield return виняток, який був вкладений у державну машину "ззовні", за допомогою коду, що приводить до ітерації. Але мої причини того, що я цього хотів, досить неясні.)

Насправді один із запитань щодо відповіді Джона стосується кидання виразу return return.

Очевидно, прибутковість 10 не така вже й погана. Але це було б погано:

yield return File.ReadAllText("c:\\missing.txt").Length;

Тож чи не було б більше сенсу оцінювати це всередині попереднього блоку try / catch:

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
        __current = File.ReadAllText("c:\\missing.txt").Length;
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    return true;

Наступною проблемою будуть вкладені блоки try / catch та повторне вилучення винятків:

try
{
    Console.WriteLine("x");

    try
    {
        Console.WriteLine("a");
        yield return 10;
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        Console.WriteLine("y");

        if ((DateTime.Now.Second % 2) == 0)
            throw;
    }
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

Але я впевнений, що це можливо ...


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