本節では例外についてまとめてみます。
例外とは、コンパイル時に発見される問題ではなく、
実行時に起こるエラー(異常)のことです。
C#に限らず、
例外処理の仕組みがある言語を利用する際は
積極的にこの機能を使うことで
実行時のエラーに対処するコードを
簡潔にスッキリ書くことができます。
例えば、連続する10個の処理があるとします。
1つ1つの処理ごとにパラメータや戻り値、
処理そのものに異常がないかどうかチェックして
異常が発見されれば、それに対処するコードを書く。
同様のコードが10箇所に記述されると
本来何をしているコードなのか可読性が失われますし
同じことを何度も書くのは面倒な作業です。
例外処理を行うことで
実行時に起こりうる異常に対応するコードを
スッキリ書くことができます。
なお、発生した例外を捕捉せず放ってしまうと
実行時にエラーとなり以後の処理が中断されます。
アプリの場合はそのまま終了となります。
例外を捕捉するには、try と catchを使います。
例外を捕捉したいコードの範囲をtryで囲んで
実際に例外が発生するとcatchで捕捉します。
では、ポピュラーな例として
0で除算を行って例外が発生するコードを書いてみます。
【例外を捕捉するコード】
try
{
int a = 10, b = 0;
int c = a / b; //0で除算しているので実行時に例外発生
Console.WriteLine( "処理終了" ); //ここは実行されない
}
catch
{
Console.WriteLine( "例外が捕捉されました。" );
}
【実行結果】
例外が捕捉されました。
【書式】
try
{
例外を捕捉したいコード
}
catch
{
例外が捕捉された場合に実行するコード
}
まず、tryで
例外を捕捉したい部分のコードを{}で囲みます。
次に、それと対にして
例外が発生し、それが捕捉された場合にどう振舞うかを
次のcatchブロックの中に記述します。
このように、
ほとんどの場合tryとcatchはペアで記述します。
上記サンプルは文法的には正しいので
コンパイルエラーは起こりません。
実行時に、c = a / b(0での除算)を行ったところで例外が発生します。
発生した例外はすぐさまcatchで捕捉されるので
その次の行の"処理終了"は出力されません。
そして、例外が捕捉されると
catchブロックの中のコードが実行されます。
上記は極端で強引なサンプルですが、例えば
ファイルをオープンして読み書きを行おうと試みたが
何かの原因で失敗した場合、
そのファイルをクローズして後始末をする、
といった例が考えられます。
データベースへのアクセスや通信の世界の話でも同様です。
例外処理は様々なケースで有効に利用できます。
【補足】
例外を捕捉(catch)することで
本来であればそこで処理が中断されるようなケースを救い
その後の処理を続行できるメリットがあります。
catchされた後は、
catchブロックの後の処理が続行されます。
また、次節で説明しますが
例外には種類があります。
本例ではcatchの後に例外の種類の指定がないので
ここではすべての例外が捕捉されます。
例外には種類があります。
前述の項では、例外は単に例外として処理され
それがどんな種類の例外であるのか
また、発生した例外の種類に応じて
それぞれに対応するための適切な処理が行われていません。
ここでは例外の種類について説明します。
まず、C#では例外クラスが用意されています。
色々な種類の例外がありますが
すべての例外の源となるクラスは
System.Exception クラス
です。
そして、前述の例で扱った0で除算した際の例外は
System.DivideByZeroException クラス
として定義されています。
それでは、前述のすべての例外を捕捉するサンプルを
0で除算した際の例外だけを捕捉するように書き換えてみます。
【0での除算例外を捕捉するコード】
try
{
int a = 10, b = 0;
int c = a / b;
Console.WriteLine( "処理終了" );
}
catch(DivideByZeroException ex)
{
// Messageプロパティーに例外内容文字列が格納されているので出力
Console.WriteLine( ex.Message );
}
【実行結果】
0 で除算しようとしました。
【書式】
try
{
例外を捕捉したいコード
}
catch( 例外クラス名 例外変数名 )
{
例外が捕捉された場合に実行するコード
}
catchのところに
捕捉したい例外クラスを記述する点がポイントです。
どんな種類の例外が発生したのかがはっきりしたことで
より適切な対処ができるようになりました。
実行時に発生する例外は1種類とは限りません。
0での除算、オーバーフロー、ファイルアクセス時の異常、などなど
様々な例外が発生します。
ここでは複数の例外を捕捉する方法を示します。
具体的には
catch節を捕捉したい例外の種類の数だけ連続して記述します。
【複数の例外を捕捉するコード】
try
{
int a = 10, b = 0;
int c = a / b;
Console.WriteLine( "処理終了" );
}
catch(DivideByZeroException ex)
{
// 0での除算を捕捉
Console.WriteLine( ex.Message );
}
catch(OverflowException ex)
{
// オーバーフローを捕捉
Console.WriteLine( ex.Message );
}
catch(Exception ex)
{
// その他全般の例外を捕捉
Console.WriteLine( ex.Message );
}
【実行結果】
0 で除算しようとしました。
この時の動きですが、
上記例ではtry節の中で0で除算を行い
DivideByZeroException例外が発生します。
すると、
catch(DivideByZeroException ex)
{
// 0での除算を捕捉
Console.WriteLine( ex.Message );
}
の部分のコードだけが実行され
その他のcatch説のコードは実行されません。
このように例外の種類に応じて
それぞれのケースに応じて適切な処理を記述することができます。
なお、上記サンプルでは
0での除算とオーバーフローだけが警戒されていますが
万が一、それ以外の例外が発生することを考えて
コードの最後の方で
その他全般の例外が捕捉されるようにしています。
例外は明示的に投げることも可能です。
throwを使います。
【明示的に例外を投げるコード】
try
{
// 中略
throw new Exception("あぼーん");
// 中略
}
catch( Exception ex )
{
Console.WriteLine( ex.Message );
}
【実行結果】
あぼーん
【書式】
throw 例外オブジェクト;
【補足】
throwする際は、
パラメータとして例外クラスのオブジェクトを投げるので
例外クラスをnewしている点に注意してください。
throw;
として、throw単文でも例外を投げることは可能ですが
その場合、それを捕捉するためのcatchは
catch
{
}
或いは
catch( Exception ex )
{
}
のいずれかとなります。(どちらでも良いです)
色々な例外クラスがありますが
上記コードではExceptionクラスを用い、
さらにコンストラクタの引数として
エラーメッセージを設定しています。
他にどのような例外クラスがあるのかについては
MSDNなどを参照してみてください。
また、catchした例外を
さらにthrowして外側のtry〜catchへ通知することも可能です。
(次節参照)
try〜catchはネストして記述可能です。
【関数内で完結したtry〜catchのネストコード】
static void Main(string[] args)
{
try
{
try
{
int a = 10, b = 0;
int c = a / b;
Console.WriteLine( "処理終了" );
}
catch(OverflowException ex)
{
// オーバーフローを捕捉
Console.WriteLine( ex.Message );
}
}
catch(Exception ex)
{
// 内側のcatchで捕捉できなかったその他全般の例外を捕捉
Console.WriteLine( ex.Message );
}
}
【実行結果】
0 で除算しようとしました。
極端な例ですが、上記例では、
内側のtry節の中で0で除算が行われ
DivideByZeroException例外が発生します。
しかし、内側のcatch節はこの例外を処理しておらず
(対処しているのはOverflowException例外のみ)
外側のcatch節で捕捉されます。
また、catchした例外を
さらに外側のcatchへthrowすることも可能です。
【おまけ】
次に示すコードでは2つのクラスが登場します。
ひとつはアプリケーションの最初に実行される
エントリポイント(Main関数)を持ったクラスです。
もうひとつは例外を発生させるクラスです。
発生した例外がどのようにcatchされるのか
また、catchされた例外がさらにthrowされ、
それがまたどこでcatchされるのか
デバッガーを使って順序を確認してみてください。
参考になると思います。
【例外が通知される様子を確認するコード】
using System;
namespace Sample
{
///
/// 例外を起こすサンプルクラス
///
class Class2
{
public void Func()
{
try
{
int a = 10, b = 0;
int c = a / b; // 1.最初にここで例外発生
Console.WriteLine( "処理終了" );
}
catch(Exception ex)
{
Console.WriteLine(ex.Message); // 2.ここで最初にcatchされます
throw; // 3.外側のtryへ例外をthrow
}
}
}
class Class1
{
///
/// アプリケーションのメイン エントリ ポイントです。
///
[STAThread]
static void Main(string[] args)
{
try
{
// Class2のインスタンス生成
Class2 c2 = new Class2();
// Class2のFunc関数を呼び出し
c2.Func();
}
catch(Exception ex)
{
Console.WriteLine(ex.Message); // 4.Class2からのthrowをcatch
}
}
}
}
【実行結果】
0 で除算しようとしました。
0 で除算しようとしました。
C#の例外処理には
さらに便利なfinallyが用意されています。
その名の通り、
finallyブロックのコードは最後に実行されます。
これは、例外が発生した場合も、そうでない場合も関係無く
どちらにせよ必ず実行されます。
例えば、ファイルをオープンして読み書きを行う場合
処理の過程で例外が発生してもしなくても
作法として最後にファイルは閉じなければなりません。
また、データベースとの接続や
ネットワーク処理においては
最後に接続を切断します。
このように
必ず最後に実行したい処理をfinallyブロックに記述すると便利です。
具体的に例を示します。
テキストファイルをオープンし、内容を出力します。
ファイルが存在しなければ例外が発生します。
また、読み込みの途中で例外がするかもしれません。
いずれにしても、例外が発生してもしなくても
最後にファイルがクローズされるよう
finallyでクローズ処理を記述しています。
【テキストファイルの内容を出力するコード】
using System;
namespace Sample
{
class Class1
{
///
/// アプリケーションのメイン エントリ ポイントです。
///
[STAThread]
static void Main(string[] args)
{
// ファイルを読み込むためのStreamReaderクラス
System.IO.StreamReader reader = null;
try
{
// テキストファイルと関連付け。
// ここでテキストファイルが存在しなければ
// System.IO.FileNotFoundException 例外が発生します。
reader = new System.IO.StreamReader( "c:\\sample.txt" );
// テキストファイルの全内容を一気に読み込み
string strText = reader.ReadToEnd();
// 読み込んだ内容を出力
Console.WriteLine( strText );
}
catch(Exception ex)
{
Console.WriteLine(ex.GetType().FullName + " 例外が発生しました。");
Console.WriteLine("例外内容:" + ex.Message);
}
finally
{
// 例外が発生してもしなくてもファイルをクローズする。
// 但し、readerが有効であることを確認する。
if ( reader != null )
reader.Close();
}
}
}
}
【実行結果1:テキストファイルが存在しない場合】
System.IO.FileNotFoundException 例外が発生しました。
例外内容:ファイル "c:\sample.txt" が見つかりませんでした。
【実行結果2:テキストファイルが存在する場合】
abc
※補足
テキストファイルの内容により異なります。
テキストファイルを用意する場合は
テキストは短めの半角文字を記述してください。
【書式】
try ※tryは必ず必要
{
}
catch ※catchの記述は任意
{
}
finally
{
// 最後に実行するコード
}
C#では、演算のオーバーフロー(桁溢れ)に対して
例外を発生させるか否かを指定することができます。
それには、checked と unchecked を使います。
まず、わざとオーバーフローが発生するコードを書いてみます。
byte型は符号なしの8バイト。
値の許容範囲は0〜255です。
ここに、128+128の結果256を代入してオーバーフローさせます。
結果は切捨てられ、0が出力されます。
【Sampleコード】
using System;
using System.Collections.Generic;
using System.Text;
namespace over
{
class Program
{
static void Main(string[] args)
{
byte bret, b1 = 128, b2 = 128;
try
{
bret = (byte)(b1 + b2);
Console.WriteLine("{0}", bret);
}
catch (OverflowException ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
【実行結果】
0
続行するには何かキーを押してください . . .
オーバーフローの例外を補足するためにcatch節を記述しましたが
catchされませんでした。
サンプルコードは簡単なものでしたが
これが複雑なプログラムであったなら、バグの原因となったり
意図しない結果が待っています。
これを、明示的にオーバーフローが発生した場合は
例外が発生するようにcheckedを使って書き換えてみます。
具体的には、演算式部分をchecked()で囲みます。
すると、オーバーフローの例外が発生します。
【例外を発生させるコード】
using System;
using System.Collections.Generic;
using System.Text;
namespace over
{
class Program
{
static void Main(string[] args)
{
byte bret, b1 = 128, b2 = 128;
try
{
checked(bret = (byte)(b1 + b2)); //ここに注目
Console.WriteLine("{0}", bret);
}
catch (OverflowException ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
【実行結果】
算術演算の結果オーバーフローが発生しました。
続行するには何かキーを押してください . . .
【checked書式1】
checked( 式 )
また、checkedでオーバーフローの例外を発生させたい部分を
範囲指定することも可能です。
【checked書式2】
checked
{
中略
}
逆に、オーバーフローの例外を発生させたくない場合は
uncheckedを指定します。
書式はcheckedと同様です。
例外は発生しなくなりますが、
計算結果がオーバーフローした場合は
結果は切り捨てられることに注意してください。
【unchecked書式1】
unchecked( 式 )
【unchecked書式2】
unchecked
{
中略
}