C#では、なぜtryブロック内で宣言された変数のスコープが制限されていますか?


23

エラー処理を追加したい:

var firstVariable = 1;
var secondVariable = firstVariable;

以下はコンパイルされません:

try
{
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

他のコードブロックが行うように、try catchブロックが変数のスコープに影響を及ぼす必要があるのはなぜですか?一貫性を別にして、リファクタリングなしでコードをエラー処理でラップできるのは理にかなっていますか?


14
A try.. catchは特定のタイプのコードブロックであり、すべてのコードブロックに関する限り、1つの変数を宣言して、同じ変数を別のスコープでスコープの問題として使用することはできません。
ニール

「特定のタイプのコードブロックです」。どのように具体的ですか?ありがとう
-J –Mᴇᴇ

7
中括弧の間はコードブロックです。ifステートメントとforステートメントの後に表示されますが、概念は同じです。コンテンツは、その親スコープに対して上位スコープにあります。{}試行なしで中括弧を使用した場合、これは問題になると確信しています。
ニール

ヒント:using(IDisposable){}と{}のみが同様に適用されることに注意してください。IDisposableでusingを使用すると、成功または失敗に関係なく、リソースが自動的にクリーンアップされます。これには、いくつかの例外が...あなたがIDisposableインターを実装期待ではないすべてのクラスのように、あります
ジュリア・マクギガン

1
:ここStackOverflowの、上のこの同じ質問に関する議論の多くstackoverflow.com/questions/94977/...
ジョン・シュナイダー

回答:


90

コードが次の場合:

try
{
   MethodThatMightThrow();
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

これでfirstVariable、メソッド呼び出しがスローされた場合、未宣言の変数()を使用しようとすることになります。

:上記の例は、元の質問に具体的に答えており、「consistency-sake aside」と記載されています。これは、一貫性以外の理由があることを示しています。しかし、ピーターの答えが示すように、一貫性からの強力な議論あります。これは確かに決定において非常に重要な要素だったでしょう。


ああ、これはまさに私が望んでいたことです。私が提案していたことを不可能にするいくつかの言語機能があることは知っていましたが、シナリオを思い付くことができませんでした。どうもありがとう。
JᴀʏMᴇᴇ

1
「メソッド呼び出しがスローされた場合、宣言されていない変数を使用しようとすることになります。」さらに、スローされるコードの前に変数が宣言されているが初期化されていないかのように変数を処理することにより、これが回避されたと仮定します。その後、宣言されないことはありませんが、潜在的に割り当てられない可能性があり、明確な割り当て分析はその値を読み取ることを禁止します(発生する可能性のある介在する割り当てなしで)。
エリアケイガン

3
C#のような静的言語の場合、宣言は実際にはコンパイル時にのみ関連します。コンパイラは、スコープ内で宣言を簡単に移動できます。実行時のより重要な事実は、変数が初期化されない可能性があることです。
jpmc26

3
私はこの答えに同意しません。C#には、初期化されていない変数を読み取ることができないというルールが既にありますが、データフローを認識しています。(aの場合に変数を宣言switchし、他の変数にアクセスしてみてください。)この規則はここで簡単に適用でき、このコードのコンパイルを防ぐことができます。以下のピーターの答えは、もっともらしいと思います。
セバスチャンレッド

2
宣言されていないものと初期化されていないもの、およびC#が別々に追跡するものには違いがあります。宣言されたブロックの外部で変数を使用することが許可された場合、最初のcatchブロックで変数に割り当てることができ、2番目のtryブロックで確実に割り当てられます。
svick

64

私はこれがベンによって十分に回答されたことを知っていますが、私は都合よく押しのけられた一貫性のあるPOVに対処したかったのです。try/catchブロックがスコープに影響を与えないと仮定すると、次のようになります。

{
    // new scope here
}

try
{
   // Not new scope
}

そして私にとって、これは 少なくとも驚き(POLA)の原理あなたが今持っているので、{および}それらに先行するもののコンテキストに応じて二重の義務を行います。

この混乱から抜け出す唯一の方法は、他のマーカーを指定してtry/catchブロックを描くことです。これは、コードの匂いを追加し始めます。したがって、あなたがtry/catch言語にスコープレスを持っている時までに、それはあなたがスコープ付きバージョンでより良いはずだったそのような混乱であったでしょう。


別の優れた答え。そして、私はポーラのことを聞いたことがありませんでした。どうもありがとう。
JᴀʏMᴇᴇ

「この混乱から抜け出す唯一の方法は、他のマーカーを指定して、輪郭を描くtry/ catchブロックすることです。」-どういう意味try { { // scope } }ですか?:)
CompuChip

@CompuChipは{}、コンテキストに応じてスコープを作成するのではなく、スコープを作成するのではなく、スコープとして二重の義務を果たします。try^ //no-scope ^別のマーカーの例になります。
レリエル

1
私の意見では、これははるかに根本的な理由であり、「本当の」答えに近いものです。
ジャックエイドリー

特に@JackAidleyは、割り当てられていない可能性のある変数を使用するコードを既に記述できるためです。したがって、Benの答えには、これがどのように役立つ振る舞いであるかについてのポイントがありますが、振る舞いがなぜ存在するのかはわかりません。ベンの答えは、OPが「一貫性のために」と言っていることに注意していますが、一貫性は完全に良い理由です!狭い範囲には、他のあらゆる種類の利点があります。
キャット

21

一貫性を別にして、リファクタリングなしでコードをエラー処理でラップできるのは理にかなっていますか?

これに答えるためには、単なる変数のスコープ以上のものを見る必要があります

変数がスコープ内に残っていても、確実に割り当てられるわけではありません。

tryブロックで変数を宣言すると、そのブロック内でのみ意味があることがコンパイラーおよび人間の読者に表明されます。コンパイラーがそれを強制すると便利です。

tryブロックの後に変数をスコープに入れたい場合は、ブロックの外で変数を宣言できます。

var zerothVariable = 1_000_000_000_000L;
int firstVariable;

try {
    // Change checked to unchecked to allow the overflow without throwing.
    firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
    Console.Error.WriteLine(e.Message);
    Environment.Exit(1);
}

これは、変数がtryブロックの外で意味があるかもしれないことを表しています。コンパイラはこれを許可します。

ただし、変数をtryブロックに導入した後、変数をスコープ内に保持することが通常は有用ではない別の理由も示しています。C#コンパイラは、明確な代入分析を実行し、値が与えられたことが証明されていない変数の値の読み取りを禁止します。したがって、変数から読み取ることはできません。

tryブロックの後に変数から読み取ろうとするとします。

Console.WriteLine(firstVariable);

それは与えるコンパイル時エラーを

CS0165未割り当てのローカル変数 'firstVariable'の使用

私はと呼ばれるEnvironment.Exitを catchブロックで、そのI、変数の前Console.WriteLineをへの呼び出しに割り当てられている知っています。しかし、コンパイラはこれを推測しません。

コンパイラがなぜそれほど厳密なのですか?

私もこれを行うことができません:

int n;

try {
    n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}

Console.WriteLine(n);

この制限を確認する1つの方法は、C#の明確な割り当て分析はあまり洗練されていないということです。しかし、別の見方をすると、catch句を使用してtryブロックにコードを記述する場合、コンパイラーとすべての人間の読者に、すべて実行できない可能性があるように扱う必要があることを伝えます。

私が意味することを説明するために、コンパイラーが上記のコードを許可したが、例外をスローしないことが個人的にわかっている関数にtryブロックで呼び出しを追加した場合を想像してください。呼び出された関数が投げていなかったことを保証することができないIOException、それを知っていることができませんでしたコンパイラがn割り当てられ、そしてその後、あなたはリファクタリングする必要があります。

つまり、tryブロックでcatch句を使用して割り当てられた変数が後で確実に割り当てられているかどうかを判断する前述の非常に高度な分析により、コンパイラは、後で壊れる可能性のあるコードの記述を回避できます。(結局、例外をキャッチすると、通常、例外がスローされる可能性があると思われます。)

すべてのコードパスを通じて変数が割り当てられていることを確認できます。

tryブロックの前またはcatchブロックで変数に値を指定することにより、コードをコンパイルできます。そうすれば、tryブロックでの割り当てが行われなくても、初期化または割り当てられたままになります。例えば:

var n = 0; // But is this meaningful, or just covering a bug?

try {
    n = 10;
}
catch (IOException) {
}

Console.WriteLine(n);

または:

int n;

try {
    n = 10;
}
catch (IOException) {
    n = 0; // But is this meaningful, or just covering a bug?
}

Console.WriteLine(n);

それらはコンパイルします。しかし、それはあなたが与えるデフォルト値は、それが理にかなっている場合のみ、そのような何かを行うことが最善である*と正しい動作を生成します。

なお、あなたはのtry-catch後の変数を読むことができますが、あなたは、tryブロックで、すべてのcatchブロックで変数を割り当てるこの第二の場合には、あなたはまだ添付内の変数を読み取ることができませんfinallyブロックので、実行は、私たちがよく考えているよりも多くの状況でtryブロックを残すことができます

* ところで、CやC ++などの一部の言語は、初期化されていない変数許可し、それらからの読み取りを防ぐための明確な割り当て分析を行いません。初期化されていないメモリを読み込むと、プログラムが非決定的で不安定な動作をするため、一般に、初期化子を提供せずにこれらの言語で変数導入することを避けることお勧めします。C#やJavaのような明確な割り当て分析を備えた言語では、コンパイラは初期化されていない変数を読み取ったり、後で意味のある値と誤って解釈される可能性のある無意味な値で初期化したりすることを防ぎます。

変数が割り当てられていないコードパスが例外をスロー(またはリターン)するようにできます。

何らかのアクション(ロギングなど)を実行して例外を再スローまたは別の例外をスローする場合、変数が割り当てられていないcatch句でこれが発生すると、コンパイラーは変数が割り当てられたことを認識します。

int n;

try {
    n = 10;
}
catch (IOException e) {
    Console.Error.WriteLine(e.Message);
    throw;
}

Console.WriteLine(n);

それはコンパイルされ、妥当な選択になるでしょう。例外がスローさだけにされていない限り、しかし、実際のアプリケーションでは、それも回復しようとするのは意味がない状況*、あなたはまだキャッチし、それを適切に処理していることを確認する必要があり、どこか

(この状況でもfinallyブロックの変数を読み取ることはできませんが、結局のところ、finallyブロックは本質的に常に実行され、この場合、変数は常に割り当てられるとは限りません)

* たとえば、多くのアプリケーションが処理するcatch句を持っていないのOutOfMemoryExceptionので、彼らは何でも可能性があり、それについて何があるかもしれないクラッシュとして少なくとも悪いよう

たぶん、あなたは本当にコードをリファクタリングたいと思うでしょう。

この例では、tryブロックを導入firstVariablesecondVariableます。私が言ったように、それらが後にスコープ内に残るように、それらが割り当てられるtryブロックの前にそれらを定義することができ、あなたはそれらが常に割り当てられることを確認することによってそれらから読むことを許可するようにコンパイラを満足/トリックすることができます。

しかし、これらのブロックの後に表示されるコードは、おそらくそれらが正しく割り当てられていることに依存しています。その場合、コードはそれを反映し、確認する必要があります。

まず、そこでエラーを実際に処理できますか? 例外処理が存在する理由の1つは、エラー発生した場所に近くなくても、エラーを効果的処理できる場所でエラーを処理しやすくすることです。

これらの変数を初期化して使用する関数のエラーを実際に処理できない場合、tryブロックはその関数内にあるのではなく、どこかより高い場所(つまり、その関数を呼び出すコード、またはコード内)呼び出しているというコード)。ちょうどあなたが誤ってどこかにスローされた例外をキャッチし、誤って初期化する際に、それがスローされたと仮定していない作りfirstVariablesecondVariable

別の方法は、変数を使用するコードをtryブロックに配置することです。これはしばしば合理的です。繰り返しますが、イニシャライザーからキャッチしている同じ例外が周囲のコードからもスローされる可能性がある場合は、それらを処理するときにその可能性を無視しないようにしてください。

(実際に例外をスローできるように、例で示されているよりも複雑な式で変数を初期化しており、実際にすべての可能な例外をキャッチするのではなく、特定の例外をキャッチすることを計画していると仮定していますあなたがすることができます予測し、有意義に扱う。それは事実だ現実の世界はいつもとても素敵ではなく、生産コードは時々これを行いますが、ここではあなたの目標であるため、2つの特定の変数の初期化中に発生するエラーを処理するために、任意のcatch節あなたは、その特定のために書きます目的は、それらがどのようなエラーであっても固有のものでなければなりません。

3番目の方法は、失敗する可能性のあるコードと、それを処理するtry-catchを独自のメソッドに抽出することです。これは、最初にエラーを完全に処理し、次にどこかで処理する必要がある例外を誤ってキャッチする心配がない場合に役立ちます。

たとえば、いずれかの変数の割り当てに失敗した場合、アプリケーションをすぐに終了するとします。(明らかに、すべての例外処理が致命的なエラーに対応しているわけではありません。これは単なる例であり、アプリケーションが問題にどのように反応するかは異なる場合があります。)

// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
    try {
        // This code is contrived. The idea here is that obtaining the values
        // could actually fail, and throw a SomeSpecificException.
        var firstVariable = 1;
        var secondVariable = firstVariable;
        return (firstVariable, secondVariable);
    }
    catch (SomeSpecificException e) {
        Console.Error.WriteLine(e.Message);
        Environment.Exit(1);
        throw new InvalidOperationException(); // unreachable
    }
}

// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
    var (firstVariable, secondVariable) = GetFirstAndSecondValues();

    // Code that does something with them...
}

このコードは、複数の値を返すC#7.0の構文でValueTupleを返し、分解しますが、まだ以前のバージョンのC#を使用している場合は、この手法を使用できます。たとえば、出力パラメータを使用するか、両方の値を提供するカスタムオブジェクトを返すことができます。さらに、2つの変数が実際に密接に関連していない場合は、とにかく2つの別々のメソッドを用意する方が良いでしょう

特にそのようなメソッドが複数ある場合、致命的なエラーをユーザーに通知して終了するためにコードを集中化することを検討する必要があります。(たとえば、あなたが書くことができるDieとのメソッドmessageのパラメータを。)ラインが実際に実行されることはありませんあなたがする必要はありませんので(とはならない)それのためのcatch句を記述します。throw new InvalidOperationException();

特定のエラーが発生したときに終了するだけでなく、元の例外をラップする別のタイプの例外をスローすると、次のようなコードを記述することがあります。(その場合、2番目の到達不能なthrow式必要ありません。)

結論:スコープは全体像の一部にすぎません。

変数の宣言を代入から分離するだけで、リファクタリングせずに(または必要に応じてリファクタリングをほとんど行わずに)エラー処理でコードをラップする効果を得ることができます。コンパイラは、C#の明確な割り当て規則を満たす場合にこれを許可し、tryブロックの前で変数を宣言すると、その大きなスコープが明確になります。しかし、さらにリファクタリングすることが最良の選択肢かもしれません。


「catch句を使用してtryブロックにコードを記述する場合、コンパイラと人間の読者の両方に、すべて実行できない可能性があるように扱う必要があることを伝えています。」コンパイラが気にするのは、前のステートメントが例外をスローした場合でも、制御が後のステートメントに到達できることです。コンパイラは通常、1つのステートメントが例外をスローした場合、次のステートメントは実行されないため、割り当てられていない変数は読み取られないと想定します。「キャッチ」を追加すると、コントロールが後のステートメントに到達できるようになります。問題は、tryブロック内のコードがスローするかどうかではなく、キャッチです。
ピートカーカム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.