キャッチ付きのtryブロック内にreturnが表示されないのはなぜですか?


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);
}

(議論のために)WriteLinetryブロック内の呼び出しのいずれかによって例外がスローされたと仮定します。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/...
ローマStarkov

注:catchブロック自体でも譲ることはできません:-(
Simon_Weaver

2
oldnewthingリンクは機能しなくなりました。
Sebastian Redl

回答:


50

これは実現可能性というより実用性の問題だと思います。この制限が実際には回避できない問題である場合が非常に少数あると思いますが、コンパイラの複雑さが非常に大きくなるでしょう。

私がすでに遭遇したこのようないくつかのものがあります:

  • 属性をジェネリックにすることができない
  • XがXY(Xのネストされたクラス)から派生できない
  • 生成されたクラスのパブリックフィールドを使用するイテレータブロック

これらのいずれの場合でも、コンパイラーがさらに複雑になる代わりに、もう少し自由度を上げることができます。チームが実用的な選択をしたので、私は彼らを称賛します。99.9%の正確なコンパイラーを備えたやや制限のある言語がいいです(そうです、バグがあります。先日、SOで1つに遭遇しました)。正しくコンパイルできなかった柔軟な言語。

編集:これがなぜそれが実現可能であるかについての疑似証明です。

次の点を考慮してください。

  • イールドリターンパーツ自体が例外をスローしないことを確認できます(値を事前計算してから、フィールドを設定して「true」を返すだけです)。
  • イテレータブロックでyield returnを使用しないtry / catchが許可されています。
  • イテレータブロックのすべてのローカル変数は、生成された型のインスタンス変数であるため、コードを新しいメソッドに自由に移動できます

次に変換します。

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
概念実証には適していますがusing、andのようなスコープを作成し始めると、この戦略は困難になります(おそらくC#コンパイラライターよりもC#プログラマーにとっての負担が大きくなります)foreach。例:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian

「try / catch」の通常のセマンティクスは、例外が原因でtry / catchブロックの一部がスキップされた場合、存在する場合、制御が適切な「catch」ブロックに移ることを意味します。残念ながら、イールドリターンの「発生」中に例外が発生した場合、イテレータは、例外が原因で破棄されているケースと、所有者が対象のすべてのデータを取得したために破棄されているケースを区別できません。
スーパーキャット'27 / 10/27

7
「この制限が実際には回避できない問題である場合が非常に少ないのではないかと思います」これは、Cで一般的に使用されるエラーコードを返す方法を使用できるため、例外は必要ないと言っているようなものです。何年も前に。私は技術的な困難が重大である可能性があることを認めますがyield、私の意見では、それを回避するために記述しなければならないスパゲッティコードのため、これはまだの有用性を厳しく制限しています。
jpmc26 2013

@ jpmc26:いいえ、それはまったくそのように言うのとはまったく異なります。私はこれを私に噛んだことを思い出せません、そして私は何度もイテレーターブロックを使いました。それはIMO の有用性をわずかに制限しますyield-それは深刻から遠く離れています。
Jon Skeet 2013

2
この「機能」を実際に見る、この問題を回避するには、いくつかのケースでは、いくつかのかなり醜いコードを必要stackoverflow.com/questions/5067188/...
namey

5

yieldイテレーター定義内のすべてのステートメントは、ステートマシンの状態に変換されます。ステートマシンは、switchステートメントを効果的に使用して状態を進めます。それは場合はやったのコード生成yieldのtry / catch内の文をそれが複製しなければならないすべてtryのためのブロックをそれぞれ yield他のすべて除外して声明yieldそのブロックの声明を。特に、あるyieldステートメントが前のステートメントに依存している場合は、これが常に可能であるとは限りません。


2
買わないと思います。それは完全に実現可能だと思いますが、非常に複雑です。
Jon Skeet

2
C#のtry / catchブロックは、再入可能にするためのものではありません。それらを分割すると、例外の後でMoveNext()を呼び出して、無効な可能性がある状態でtryブロックを続行できます。
Mark Cidade、

2

列挙子からの戻りを生成するときのコールスタックの巻き方/巻き戻し方が原因で、try / catchブロックが実際に例外を「キャッチ」することが不可能になると推測します。(彼が反復ブロックを作成したとしても、yield returnブロックがスタック上にないため)

イテレーターブロックとそのイテレーターを使用するforeachのセットアップについて私が話していることのアイデアを得るために。呼び出しスタックがforeachブロック内でどのように見えるかを確認してから、イテレータのtry / finallyブロック内で確認してください。


私はC ++でのスタックの巻き戻しに精通しています。C++では、デストラクタはスコープ外に出たローカルオブジェクトで呼び出されます。C#で対応するのは、try / finallyです。しかし、利回りが発生しても、その巻き戻しは発生しません。そして、try / catchの場合、利回りのリターンと相互作用する必要はありません。
Daniel Earwicker 2008

イテレータをループするときにコールスタックがどうなるかを確認すると、私が何を意味しているのか理解できるでしょう
Radu094

@ Radu094:いいえ、それは可能だと確信しています。少なくとも最終的にはそれが最終的に処理されていることを忘れないでください。
Jon Skeet

2

Microsoftの誰かがアイデアに冷たい水を注いでくれるまで、私はTHE INVINCIBLE SKEETの答えを受け入れました。しかし、私は意見の問題の部分に同意しません-もちろん、正しいコンパイラは完全なコンパイラよりも重要ですが、C#コンパイラは、この変換を私たちのために整理するのにすでに非常に巧妙です。この場合、もう少し完全性があれば、言語は使いやすくなり、教え、説明され、エッジケースや問題が少なくなります。だから私はそれは余分な努力の価値があると思います。レドモンドの数人の男が2週間頭を悩ませています。その結果、次の10年間で何百万人ものプログラマーがもう少しリラックスできるようになります。

(またyield return、反復を実行するコードによって、「外部から」ステートマシンに詰め込まれた例外をスローする方法を作りたいという厳しい要望も抱いています。しかし、これを望んでいる私の理由は非常に曖昧です。)

実際、Jonの答えについて私が持っている1つのクエリは、yield戻り式のスローを行うことです。

明らかに利回りリターン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に入れます。変数設定をどこに置くかは問題ではありません。重要な点は、イールドリターンが含まれる1つのトライ/キャッチを、それらの間にイールドリターンがある2つのトライ/キャッチに効果的に分割できることです。
Jon Skeet
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.