「catch」または「finally」のスコープの「try」で変数が宣言されないのはなぜですか?


139

C#およびJava(およびその他の言語も)では、「try」ブロックで宣言された変数は、対応する「catch」または「finally」ブロックのスコープにはありません。たとえば、次のコードはコンパイルされません。

try {
  String s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

このコードでは、sはtryブロックのスコープ内にのみ存在するため、catchブロックのsへの参照でコンパイル時エラーが発生します。(Javaでは、コンパイルエラーは「sは解決できません」です。C#では、「名前 's'は現在のコンテキストに存在しません」です。)

この問題の一般的な解決策は、tryブロック内ではなく、tryブロックの直前に変数を宣言することです。

String s;
try {
  s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

ただし、少なくとも私にとっては、(1)これは不格好なソリューションのように感じられ、(2)プログラマーが意図したよりもスコープが広い変数になります(メソッドのコンテキストのみではなく、メソッドの残り全体) try-catch-finally)。

私の質問は、この言語設計の決定の背後にある根拠は何でしたか(Java、C#、および/または他の該当する言語で)?

回答:


171

2つのこと:

  1. 一般に、Javaのスコープレベルは、グローバルと機能の2つだけです。ただし、try / catchは例外です(しゃれは意図されていません)。例外がスローされ、例外オブジェクトに変数が割り当てられると、そのオブジェクト変数は「catch」セクション内でのみ使用でき、catchが完了するとすぐに破棄されます。

  2. (そして更に重要なことに)。tryブロックのどこで例外がスローされたかはわかりません。変数が宣言される前だった可能性があります。したがって、catch / finally句で使用できる変数を特定することはできません。スコープがあなたが提案したとおりである次のケースを考えてください:

    
    try
    {
        throw new ArgumentException("some operation that throws an exception");
        string s = "blah";
    }
    catch (e as ArgumentException)
    {  
        Console.Out.WriteLine(s);
    }

これは明らかに問題です。例外ハンドラに到達したとき、sは宣言されていません。キャッチは例外的な状況を処理するためのものであり、最終的に実行する必要があるため、安全であり、これをコンパイル時の問題として宣言することは、実行時よりもはるかに優れています。


55

キャッチブロックの宣言部分に到達したことをどのように確認できますか?インスタンス化が例外をスローした場合はどうなりますか?


6
えっ?変数宣言は例外をスローしません。
Joshua

6
同意すると、例外をスローする可能性があるのはインスタンス化です。
Burkhard

19

伝統的に、Cスタイルの言語では、中括弧内で何が起こるかは中括弧内に留まります。変数の存続期間をそのようなスコープ全体に広げることは、ほとんどのプログラマにとって直感的ではないと思います。try / catch / finallyブロックを別のレベルのブレースで囲むことにより、希望どおりの結果を得ることができます。例えば

... code ...
{
    string s = "test";
    try
    {
        // more code
    }
    catch(...)
    {
        Console.Out.WriteLine(s);
    }
}

編集:すべてのルールに例外があると思います。以下は有効なC ++です。

int f() { return 0; }

void main() 
{
    int y = 0;

    if (int x = f())
    {
        cout << x;
    }
    else
    {
        cout << x;
    }
}

xのスコープは、条件付き、then句、およびelse句です。


10

他の誰もが基本を持ち出しました-ブロックで起こることはブロックに留まります。しかし、.NETの場合は、コンパイラが何が起こっていると考えているかを調べると役立つ場合があります。たとえば、次のtry / catchコードを見てください(StreamReaderがブロックの外で正しく宣言されていることに注意してください)。

static void TryCatchFinally()
{
    StreamReader sr = null;
    try
    {
        sr = new StreamReader(path);
        Console.WriteLine(sr.ReadToEnd());
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
    finally
    {
        if (sr != null)
        {
            sr.Close();
        }
    }
}

これは、MSILで次のようにコンパイルされます。

.method private hidebysig static void  TryCatchFinallyDispose() cil managed
{
  // Code size       53 (0x35)    
  .maxstack  2    
  .locals init ([0] class [mscorlib]System.IO.StreamReader sr,    
           [1] class [mscorlib]System.Exception ex)    
  IL_0000:  ldnull    
  IL_0001:  stloc.0    
  .try    
  {    
    .try    
    {    
      IL_0002:  ldsfld     string UsingTest.Class1::path    
      IL_0007:  newobj     instance void [mscorlib]System.IO.StreamReader::.ctor(string)    
      IL_000c:  stloc.0    
      IL_000d:  ldloc.0    
      IL_000e:  callvirt   instance string [mscorlib]System.IO.TextReader::ReadToEnd()
      IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0018:  leave.s    IL_0028
    }  // end .try
    catch [mscorlib]System.Exception 
    {
      IL_001a:  stloc.1
      IL_001b:  ldloc.1    
      IL_001c:  callvirt   instance string [mscorlib]System.Exception::ToString()    
      IL_0021:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0026:  leave.s    IL_0028    
    }  // end handler    
    IL_0028:  leave.s    IL_0034    
  }  // end .try    
  finally    
  {    
    IL_002a:  ldloc.0    
    IL_002b:  brfalse.s  IL_0033    
    IL_002d:  ldloc.0    
    IL_002e:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()    
    IL_0033:  endfinally    
  }  // end handler    
  IL_0034:  ret    
} // end of method Class1::TryCatchFinallyDispose

何が見える?MSILはブロックを尊重します-それらは本質的に、C#をコンパイルするときに生成される基本的なコードの一部です。スコープは、C#仕様にハードセットされているだけでなく、CLRおよびCLS仕様にもあります。

スコープはあなたを保護しますが、時々それを回避する必要があります。時間が経つにつれて、それに慣れ、自然に感じ始めます。他の人が言ったように、ブロックで何が起こるかはそのブロックに留まります。何かを共有したいですか?あなたはブロックの外に出なければなりません...


8

とにかく、C ++では、自動変数のスコープは、それを囲む中括弧によって制限されます。中括弧の外でtr​​yキーワードをプランクダウンすることで、なぜこれが異なると予想されるのですか?


1
同意した 「}」はスコープの終わりを意味します。しかし、try-catch-finallyは、tryブロックの後に、catchまたはfinalブロック、あるいはその両方が必要になるという点で珍しいものです。したがって、通常のルールの例外で、tryブロックのスコープが関連付けられたcatch / finallyに受け入れられるように見える可能性がありますか?
Jon Schneider

7

ravenspointが指摘したように、誰もが変数が定義されているブロックに対してローカルであることを期待します。tryブロックを導入し、そうしcatchます。

あなたは、両方のローカル変数をしたい場合trycatch、ブロックの両方を囲む試してください。

// here is some code
{
    string s;
    try
    {

        throw new Exception(":(")
    }
    catch (Exception e)
    {
        Debug.WriteLine(s);
    }
}

5

簡単な答えは、Cとその構文を継承したほとんどの言語はブロックスコープであることです。つまり、変数が1つのブロック、つまり{}内で定義されている場合、そのスコープになります。

ちなみに例外はJavaScriptで、これは同様の構文を持っていますが、関数スコープです。JavaScriptでは、tryブロックで宣言された変数は、catchブロックのスコープ内と、それを含む関数内の他のすべての場所にあります。


4

@burkhardにはなぜ適切に回答されたのかという質問がありますが、私が追加したかったメモとして、あなたの推奨ソリューションの例は時間の99.9999 +%は良いですが、良い習慣ではありません。使用する前にnullをチェックする方がはるかに安全です。 tryブロック内でインスタンスを作成するか、tryブロックの前に宣言するのではなく、変数を何かに初期化します。例えば:

string s = String.Empty;
try
{
    //do work
}
catch
{
   //safely access s
   Console.WriteLine(s);
}

または:

string s;
try
{
    //do work
}
catch
{
   if (!String.IsNullOrEmpty(s))
   {
       //safely access s
       Console.WriteLine(s);
   }
}

これにより、回避策のスケーラビリティが提供されるため、tryブロックで行う作業が文字列の割り当てよりも複雑な場合でも、catchブロックから安全にデータにアクセスできるはずです。


4

MCTS Self-Paced Training Kit(Exam 70-536)のレッスン2 :Microsoft®.NET Framework 2.0—Application Development Foundationの「例外をスローしてキャッチする方法」というタイトルのセクションによると、例外が発生した可能性があります(他の人がすでに指摘しているように)tryブロックの変数宣言の前。

25ページからの引用:

「前の例では、StreamReader宣言がTryブロックの外に移動したことに注意してください。これは、FinallyブロックがTryブロック内で宣言された変数にアクセスできないために必要です。これは、例外が発生した場所に応じて、試行ブロックはまだ実行されていない可能性があります。」


4

だれもが指摘したように、答えはほぼ「それがブロックの定義方法」です。

コードをきれいにするためのいくつかの提案があります。ARMを参照

 try (FileReader in = makeReader(), FileWriter out = makeWriter()) {
       // code using in and out
 } catch(IOException e) {
       // ...
 }

クロージャーもこれに対処することになっています。

with(FileReader in : makeReader()) with(FileWriter out : makeWriter()) {
    // code using in and out
}

更新: ARMはJava 7で実装されています。http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html


2

あなたの解決策はまさにあなたがすべきことです。tryブロックで宣言に到達したかどうかさえわからないため、catchブロックで別の例外が発生します。

単に別のスコープとして機能する必要があります。

try
    dim i as integer = 10 / 0 ''// Throw an exception
    dim s as string = "hi"
catch (e)
    console.writeln(s) ''// Would throw another exception, if this was allowed to compile
end try

2

変数はブロックレベルで、そのTryまたはCatchブロックに制限されています。ifステートメントで変数を定義するのと同じです。この状況について考えてください。

try {    
    fileOpen("no real file Name");    
    String s = "GO TROJANS"; 
} catch (Exception) {   
    print(s); 
}

文字列は宣言されないので、依存することはできません。


2

なぜなら、tryブロックとcatchブロックは2つの異なるブロックだからです。

次のコードでは、ブロックAで定義されたsがブロックBで表示されることを期待しますか?

{ // block A
  string s = "dude";
}

{ // block B
  Console.Out.WriteLine(s); // or printf or whatever
}

2

あなたの例ではそれが機能しないのは奇妙ですが、これと同じようなものを取ってください:

    try
    {
         //Code 1
         String s = "1|2";
         //Code 2
    }
    catch
    {
         Console.WriteLine(s.Split('|')[1]);
    }

これにより、コード1が壊れた場合、キャッチはnull参照例外をスローします。これで、try / catchのセマンティクスは十分に理解されていますが、sは初期値で定義されているため、これは厄介なコーナーケースです。

繰り返しになりますが、これは理論的には分離された定義(String s; s = "1|2";)またはその他の条件セットのみを許可することで修正できますが、一般的には「いいえ」と言う方が簡単です。

さらに、スコープのセマンティクスを例外なくグローバルに定義できます。具体的には、ローカル{}はすべての場合に定義されている限り存続します。マイナーポイントですが、ポイントです。

最後に、やりたいことを行うために、try catchの前後にブラケットのセットを追加できます。読みやすさは多少犠牲になりますが、多すぎませんが、必要なスコープが得られます。

{
     String s;
     try
     {
          s = "test";
          //More code
     }
     catch
     {
          Console.WriteLine(s);
     }
}

1

あなたが与えた特定の例では、初期化sは例外をスローできません。そのため、おそらくその範囲を拡張できると思います。

ただし、一般に、初期化式は例外をスローする可能性があります。イニシャライザーが例外をスローした変数(または、それが発生した別の変数の後に宣言された変数)がcatch / finallyのスコープ内にあることは意味がありません。

また、コードの読みやすさが低下します。C(およびC ++、Java、C#を含むそれに続く言語)のルールは単純です。変数のスコープはブロックに従います。

変数をtry / catch / finallyのスコープ内に配置し、それ以外の場所には配置しない場合は、全体を別の中括弧のセット(裸のブロック)でラップし、tryの前に変数を宣言します。


1

それらが同じスコープ内にない理由の一部は、tryブロックのどの時点でも例外をスローする可能性があるためです。それらが同じスコープ内にある場合、例外がスローされた場所によってはさらにあいまいになる可能性があるため、待機中の障害となります。

少なくともtryブロックの外側で宣言されている場合は、例外がスローされたときに変数が何であるかが確実にわかります。tryブロックの前の変数の値。


1

ローカル変数を宣言すると、それはスタックに配置されます(一部のタイプでは、オブジェクトの値全体がスタックにあり、他のタイプでは参照のみがスタックにあります)。tryブロック内で例外が発生すると、ブロック内のローカル変数が解放されます。つまり、スタックは「アンワインド」されて、tryブロックの開始時の状態に戻ります。これは仕様によるものです。これは、try / catchがブロック内のすべての関数呼び出しを取り消すことができ、システムを機能状態に戻す方法です。このメカニズムがなければ、例外が発生したときに何かの状態を確認することはできません。

エラー処理コードが、tryブロック内で値が変更されている外部で宣言された変数に依存することは、私にとっては悪い設計のように思えます。あなたがやっていることは、情報を得るために意図的にリソースを漏らしていることです(この特定のケースでは、情報を漏らしているだけなのでそれほど悪くはありませんが、それが他のリソースだったとしたらどうでしょうか?未来)。エラー処理でより細かい設定が必要な場合は、tryブロックを小さなチャンクに分割することをお勧めします。


1

トライキャッチがある場合、ほとんどの場合、それがスローする可能性のあるエラーを知っている必要があります。これらの例外クラスは通常、例外について必要なすべてのものを伝えます。そうでない場合は、独自の例外クラスを作成し、その情報を渡す必要があります。そうすれば、例外は自明であるため、tryブロック内から変数を取得する必要はありません。したがって、これをたくさん行う必要がある場合は、自分が設計していることを考え、他の方法があるかどうか考えてみてください。発生する例外を予測するか、例外から発生する情報を使用してから、独自の例外を再スローすることができます。詳細は例外です。


1

他のユーザーが指摘したように、中括弧は、私が知っているほとんどすべてのCスタイル言語のスコープを定義します。

単純な変数の場合、なぜそれがスコープ内にある期間を気にするのですか?それはそれほど大きな問題ではありません。

C#では、複雑な変数の場合、IDisposableを実装する必要があります。次に、try / catch / finallyを使用して、finallyブロックでobj.Dispose()を呼び出すことができます。または、usingキーワードを使用して、コードセクションの最後でDisposeを自動的に呼び出すこともできます。


1

Pythonでは、それらを宣言する行がスローしなかった場合、catch / finallyブロックに表示されます。


1

変数の宣言より上のコードで例外がスローされた場合はどうなりますか?つまり、この場合、宣言自体は行われませんでした。

try {

       //doSomeWork // Exception is thrown in this line. 
       String s;
       //doRestOfTheWork

} catch (Exception) {
        //Use s;//Problem here
} finally {
        //Use s;//Problem here
}

1

C#仕様(15.2)「ブロックISTブロックで宣言されたローカル変数または定数の範囲を。」と述べ

(最初の例では、tryブロックは "s"が宣言されているブロックです)


0

私の考えは、tryブロックの何かが例外をトリガーしたため、その名前空間の内容は信頼できないためです。つまり、catchブロックで文字列「s」を参照すると、さらに別の例外がスローされる可能性があります。


0

コンパイルエラーがスローされず、メソッドの残りの部分で宣言できる場合、tryスコープ内でのみ宣言する方法はありません。これは、変数が存在するはずの場所を明示することを強制するものであり、仮定を行いません。


0

スコーピングブロックの問題を少しの間無視すると、コンパイラーは、明確に定義されていない状況では、もっと一生懸命働かなければなりません。これは不可能ではありませんが、スコーピングエラーにより、コードの作成者であるユーザーも、作成したコードの意味(catchブロックで文字列sがnullになる可能性がある)を理解する必要があります。コードが正当である場合、OutOfMemory例外の場合、sにメモリスロットが割り当てられることは保証されていません。

// won't compile!
try
{
    VeryLargeArray v = new VeryLargeArray(TOO_BIG_CONSTANT); // throws OutOfMemoryException
    string s = "Help";
}
catch
{
    Console.WriteLine(s); // whoops!
}

CLR(およびコンパイラー)では、変数を使用する前に変数を初期化する必要があります。提示されたcatchブロックでは、これを保証することはできません。

そのため、コンパイラーは多くの作業を行わなければならず、実際には多くの利点が得られず、おそらく混乱し、try / catchの動作が異なる理由を尋ねるようになるでしょう。

一貫性に加えて、派手なものは一切許可せず、言語全体で使用されているすでに確立されているスコープセマンティクスを遵守することで、コンパイラとCLRは、catchブロック内の変数の状態をより確実に保証できます。それが存在し、初期化されていること。

言語設計者は、問題やスコープが明確に定義されている場所で使用したりロックしたりするなど、他の構成要素を使用して優れた作業を行っていることに注意してください。これにより、より明確なコードを記述できます。

たとえば、次のIDisposableオブジェクトでキーワードを使用します。

using(Writer writer = new Writer())
{
    writer.Write("Hello");
}

以下と同等です。

Writer writer = new Writer();
try
{        
    writer.Write("Hello");
}
finally
{
    if( writer != null)
    {
        ((IDisposable)writer).Dispose();
    }
}

try / catch / finallyを理解するのが難しい場合は、リファクタリングするか、別の間接層を導入して、達成しようとしていることのセマンティクスをカプセル化する中間クラスを作成してください。実際のコードを見なければ、より具体的にすることは困難です。


0

ローカル変数の代わりに、パブリックプロパティを宣言できます。これにより、割り当てられていない変数の別の潜在的なエラーも回避されます。公開文字列S {get; セットする; }


-1

割り当て操作が失敗した場合、catchステートメントには、割り当てられていない変数へのnull参照が含まれます。


2
割り当てられていません。(インスタンス変数や静的変数とは異なり)nullでもありません。
トム・ホーティン-タックライン

-1

C#3.0:

string html = new Func<string>(() =>
{
    string webpage;

    try
    {
        using(WebClient downloader = new WebClient())
        {
            webpage = downloader.DownloadString(url);
        }
    }
    catch(WebException)
    {
        Console.WriteLine("Download failed.");  
    }

    return webpage;
})();

WTF?なぜ反対票か。カプセル化はOOPに不可欠です。きれいにも見えます。
コア

2
私は反対投票ではありませんでしたが、問題は初期化されていない文字列を返すことです。
Ben Voigt
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.