Блоки итераторов, Часть третья: почему нет yield в блоке finally?

Есть три сценария, в которых код блока итератора может исполняться в finally. Ни в одном из них возврат значения изнутри finally не является хорошей идеей, так что это полностью запрещено. Эти три сценария - (1) обычная очистка ресурсов, (2) очистка ресурсов при возникновении исключения, и (3) разрушение итератора.

Для первого сценария, предположим, что у нас есть что-то вроде

try
{
  Setup();
  yield return M();
}
finally
{
  yield return N();
  Cleanup();
}

В какую машину состояний итератора мы должны это трансформировать? Наивное желание – сделать что -то типа такого:

switch (this.state)
{
case 0: goto LABEL0;
case 1: goto LABEL1:
case 2: goto LABEL2:
case 3: goto LABEL3:
}
LABEL0:
try
{
Setup();
this.current = M();
this.state = 1;
return true; // НО НЕ ВЫПОЛНЯТЬ FINALLY!
LABEL1:
}
finally
{
  this.current = N();
  this.state = 2;
  return true;
LABEL2:
  Cleanup();
}
LABEL3:
this.state = 3;
return false;

Тут сразу есть проблема: мы не только делаем «goto» внутрь finally, но мы еще и делаем из него «return». И то и другое запрещено.

Выходить из блока finally при помощи return запрещено потому, что это криво. Предположим, происходит return из блока try, заставляя выполниться finally. Что будет, когда finally выполняет return? Оригинальное возвращаемое значение будет потеряно, а вместо него будет возвращено какое-то другое значение. Это выглядит противоестественным и чреватым ошибками, поэтому такое запрещено; вы не можете выйти из finally при помощи return. И, конечно же, вы не хотите выполнить finally после возврата из try в данном случае!

Более того, в модели обработки исключений CLR запрещено переходить при помощи goto внутрь блока try или его «обработчика» (то есть, части try или finally). Переходить из обработчика наружу тоже запрещено. В этих особых регионах есть специальный код, который должен быть исполнен при входе и выходе из региона; ни тот, ни другой нельзя пропустить при помощи goto.

Так что у нас нет навскидку очевидного способа сгенерировать верифицируемый код для этого сценария. Не отходя от кассы, мы находим множество возражений против этой возможности; нам бы пришлось либо убедить парней из CLR разрешить спагетти-коду задействовать защищённые регионы, либо придумать какую-нибудь умную технику порождения такого кода.

И это сценарий, где еще не случилось ничего плохого! Предположим, случилось чудо, и мы сумели успешно сгенерировать код для сценария 1. Теперь рассмотрим второй сценарий, в котором M бросает исключение X. Помните – блок finally «ловит» исключение и выполняет код очистки. Если код очистки бросает исключение, то оригинальное исключение выбрасывается и начинается обработка нового исключения. Если код очистки успешно завершается, то оригинальное исключение продолжает «бросаться» вверх по стеку, в поисках новых блоков finally или catch.

Предположим, код очистки не бросает исключений. Как должна выглядеть последовательность управления? Вызывающий зовёт MoveNext в первый раз. M() бросает X. Блок finally срабатывает. Блок finally вызывает N(), и возвращает наверх результаты N() !? Что случилось с X, нашим исключением? Оно что, просто ждёт где-то там, в лимбо? При следующем вызове MoveNext, должен ли начать выполняться код Сleanup(), и X внезапно вернуться к жизни и продолжить своё путешествие вверх по стеку? Это не имеет никакого смысла, два вызова MoveNext могут иметь совершенно разные стеки! Это безумие, плюс для заморочек такого рода у нас нет никакого механизма в CLR.

В-третьих, предположим, мы сумели решить все эти проблемы. Теперь подумайте, что произойдёт, когда вызывают MoveNext, M() срабатывает, управление возвращается наверх, и клиент вызывает преждевременный Dispose() у енумератора. (Наверное, ему нужен был только первый элемент). Мы генерируем метод Dispose(), который проверяет текущее состояние и выполняет все оставшиеся блоки finally. Что должен делать метод Dispose() в момент, когда он встречает yield return в оставшемся блоке finally? Мы же уже даже не в вызове MoveNext! Должны ли мы вызвать N() и проигнорировать результат? Должны ли мы вернуться из Dispose() сразу после вызова N(), или перед возвратом нужно вызвать Cleanup()? Каким именно должен быть поток исполнения в данном случае? Мы в контексте, где, возможно, уже даже не итерируемся, но всё-таки пытаемся возвращать значение.

В данном сценарии возврат значения не имеет никакого логического смысла; мы, возможно, не в той ситуации, когда вызвающий ожидает возврата значений.

Так что, коротко говоря, нам пришлось бы сделать минимум две невозможные вещи для реализации сценария, который вообще не имеет смысла. Если какая возможность и напрашивается быть отброшенной на этапе проектирования, то это она. Так что: никаких yield в блоках finally. И слава богу.

В следующий раз: теперь, когда вы всё это знаете, догадаться «почему нет yield в блоках catch» будет легко.

оригинал статьи