一貫性を別にして、リファクタリングなしでコードをエラー処理でラップできるのは理にかなっていますか?
これに答えるためには、単なる変数のスコープ以上のものを見る必要があります。
変数がスコープ内に残っていても、確実に割り当てられるわけではありません。
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ブロックを導入firstVariable
しsecondVariable
ます。私が言ったように、それらが後にスコープ内に残るように、それらが割り当てられるtryブロックの前にそれらを定義することができ、あなたはそれらが常に割り当てられることを確認することによってそれらから読むことを許可するようにコンパイラを満足/トリックすることができます。
しかし、これらのブロックの後に表示されるコードは、おそらくそれらが正しく割り当てられていることに依存しています。その場合、コードはそれを反映し、確認する必要があります。
まず、そこでエラーを実際に処理できますか? 例外処理が存在する理由の1つは、エラーが発生した場所に近くなくても、エラーを効果的に処理できる場所でエラーを処理しやすくすることです。
これらの変数を初期化して使用する関数のエラーを実際に処理できない場合、tryブロックはその関数内にあるのではなく、どこかより高い場所(つまり、その関数を呼び出すコード、またはコード内)呼び出しているというコード)。ちょうどあなたが誤ってどこかにスローされた例外をキャッチし、誤って初期化する際に、それがスローされたと仮定していない作りfirstVariable
とsecondVariable
。
別の方法は、変数を使用するコードを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ブロックの前で変数を宣言すると、その大きなスコープが明確になります。しかし、さらにリファクタリングすることが最良の選択肢かもしれません。
try.. catch
は特定のタイプのコードブロックであり、すべてのコードブロックに関する限り、1つの変数を宣言して、同じ変数を別のスコープでスコープの問題として使用することはできません。