Почему не может yield return появляться внутри блока try с уловкой?

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блоке?

Конечно, часть yield return (в настоящее время) не может ничего выбросить, но почему это должно мешать нам иметь закрывающую try/ catchдля обработки исключений, созданных до или после yield return?

Обновление: здесь есть интересный комментарий от Эрика Липперта - похоже, у них уже достаточно проблем с правильной реализацией поведения try / finally!

РЕДАКТИРОВАТЬ: страница MSDN с этой ошибкой: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Однако это не объясняет почему.

Дэниел Эрвикер
источник
2
Прямая ссылка на комментарий Эрика Липперта: blogs.msdn.com/oldnewthing/archive/2008/08/14/…
Роман Старков
примечание: вы также не можете уступить в самом блоке catch :-(
Simon_Weaver
2
Ссылка oldnewthing больше не работает.
Себастьян Редл

Ответы:

50

Я подозреваю, что это вопрос практичности, а не осуществимости. Я подозреваю , что есть очень, очень мало раз , когда это ограничение является на самом деле это вопрос , который не может работаться вокруг - но дополнительные сложности в компилятор будут весьма значительными.

Я уже сталкивался с некоторыми подобными вещами:

  • Атрибуты не могут быть общими
  • Неспособность X быть производным от XY (вложенный класс в X)
  • Итератор блокирует использование публичных полей в сгенерированных классах

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

РЕДАКТИРОВАТЬ: Вот псевдо-доказательство того, почему это возможно.

Считают, что:

  • Вы можете убедиться, что часть возврата yield сама по себе не генерирует исключение (предварительно вычислите значение, а затем вы просто устанавливаете поле и возвращаете «истина»)
  • Вам разрешено использовать команду try / catch, которая не использует yield return в блоке итератора.
  • Все локальные переменные в блоке итератора являются переменными экземпляра в сгенерированном типе, поэтому вы можете свободно перемещать код в новые методы.

Теперь трансформируем:

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

в (своего рода псевдокод):

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», если таковой существует. К сожалению, если исключение возникает «во время» возврата yield, итератор не может отличить случаи, когда оно удаляется из-за исключения, от тех, где оно удаляется, потому что владелец извлек все интересующие данные.
supercat
7
«Я подозреваю, что очень и очень редко это ограничение является проблемой, которую невозможно обойти». Это как сказать, что вам не нужны исключения, потому что вы можете использовать стратегию возврата кода ошибки, обычно используемую в C, поэтому много лет назад. Я признаю, что технические трудности могут быть значительными, но yield, на мой взгляд, это все еще сильно ограничивает полезность из-за спагетти-кода, который вам нужно написать, чтобы обойти это.
jpmc26
@ jpmc26: Нет, это совсем не то, что я сказал. Я не могу припомнить, чтобы это меня когда-либо кусало, и я использовал блоки итератора много раз. Это немного ограничивает полезность yieldIMO - это далеко не серьезно .
Джон Скит,
2
Это «особенность» на самом деле требует довольно уродливой коды в некоторых случаях решить эту проблему, см stackoverflow.com/questions/5067188/...
namey
5

Все yieldоператоры в определении итератора преобразуются в состояние в конечном автомате, который эффективно использует switchоператор для продвижения состояний. Если бы он действительно сгенерировал код для yieldоператоров в try / catch, ему пришлось бы дублировать все в tryблоке для каждого yield оператора, исключая все остальные yieldоператоры для этого блока. Это не всегда возможно, особенно если одна yieldинструкция зависит от более ранней.

Марк Сидаде
источник
2
Не думаю, что куплюсь на это. Думаю, это было бы вполне осуществимо, но очень сложно.
Джон Скит,
2
Блоки try / catch в C # не предназначены для повторного входа. Если вы разделите их, можно вызвать MoveNext () после исключения и продолжить блок try с возможно недопустимым состоянием.
Марк Сидаде,
2

Я бы предположил, что из-за того, как стек вызовов запускается / раскручивается, когда вы передаете return из перечислителя, становится невозможным для блока try / catch на самом деле «поймать» исключение. (поскольку блок возврата yield не находится в стеке, даже если он создал блок итерации)

Чтобы получить представление о том, о чем я говорю, настройте блок итератора и foreach с использованием этого итератора. Проверьте, как выглядит стек вызовов внутри блока foreach, а затем проверьте его внутри блока итератора try / finally.

Radu094
источник
Я знаком с раскручиванием стека в C ++, где деструкторы вызываются для локальных объектов, выходящих за пределы области видимости. Соответствующая вещь в C # будет try / finally. Но этого раскрутки не происходит, когда происходит возврат доходности. А для try / catch нет необходимости взаимодействовать с yield return.
Дэниел Эрвикер,
Проверьте, что происходит со стеком вызовов при обходе итератора, и вы поймете, что я имею в виду
Radu094,
@ Radu094: Нет, я уверен, что это возможно. Не забывайте, что он уже обрабатывает finally, что, по крайней мере, в чем-то похоже.
Джон Скит
2

Я принимаю ответ НЕУБЕДИТЕЛЬНОГО СКИТА, пока кто-нибудь из Microsoft не придет и не обольет эту идею холодной водой. Но я не согласен с основополагающей частью - конечно, правильный компилятор важнее, чем полный, но компилятор C # уже очень умен в этом преобразовании для нас, насколько это возможно. Немного больше полноты в этом случае сделало бы язык более простым в использовании, обучении, объяснении, с меньшим количеством крайних случаев или ошибок. Так что я думаю, что это стоит дополнительных усилий. Несколько ребят из Редмонда пару недель чешут затылки, и в результате миллионы программистов в течение следующего десятилетия могут немного расслабиться.

(Я также питаю ужасное желание, чтобы был способ создать yield returnисключение, которое было вставлено в конечный автомат «извне», кодом, управляющим итерацией. Но мои причины, по которым я этого хотел, довольно неясны.)

На самом деле у меня есть один вопрос об ответе Джона, связанный с выдачей выражения yield 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
Да, вы бы поставили оценку в «try / catch». На самом деле не имеет значения, где вы поместите настройку переменной. Суть в том, что вы можете эффективно разбить одну попытку / уловку с возвратом yield в ней на две попытки / уловки с возвратом yield между ними.
Джон Скит,