例外 -基本的な例外処理-

ホームC#プログラミング応用講義 > 基本的な例外処理

目次

キーワード

例外ほどプログラマを悩ます動作はありません。プログラマ自身はそのソフトをどう使えばよいかすべて知っているので すが、ユーザはときに変な行動をとることがあります。
たとえば、数値を入力すべきところに英数字を入力したり、ないファイルを開こうとパスを指定したり....。

このような実行時の異常な動作に対応するのが例外処理と呼ばれるプログラムです。簡単な例外なら if で取ることも可能ですが、C# における例外処理は非常に強力であり、できる限り利用すべきです。if でキャッチしようとしたらあらかじめすべての例外を予想していなければなりませんが、例外は予想外のこともキャッチして処理できます。

C# における例外は4つのキーワードで成り立っています。すなわち、
try / catch / finally
throw
の4つです。throw は応用的な例外処理で利用するものなので、次の回で紹介します。

try/catch/finally はセットで利用されることが頻繁です。try が出てきたら必ず catch は出てきますし、オプションで finally を利用することもあります。
それぞれのキーワードの働きは非常に単純です。

try は例外が発生しそうなコードにつけます。この try で囲まれた部分({ と })で例外が発生すると、catch の部分にプログラムの制御が移ります
try 内で例外が発生しなくても、発生して catch に制御が移っても、finally で囲まれた部分は実行されます。ここではオブジェクトの破棄やネットワーク接続の切断・ファイルのクローズなど最終処理を行うのに利用しま す。

これらのキーワードが出てきたら例外処理であることに注意してください。

すべての例外をキャッチする

using System;

class Class1
{
static void Main(string[] args)
{
try
{
int i = int.Parse(args[0]);
const int a = 10;
Console.WriteLine(a/i);
}
catch
{
Console.WriteLine("例外発生!!");
}
}
}
今度は実際に例外をキャッチしてみましょう。ちなみに、例外が発生したときに処理を行うことを「捕まえる」という意味から、例外をキャッチすると いいます。

このプログラムはコマンドライン引数に指定した数値を入力するとその値を10で割るというプログラムです。
例外を説明する前に「const」キーワードについて説明します。

これは変数を定数にしてしまうキーワードです。変数はプログラムの途中で変えることもできま すが、変わらないほうがいい場合もあります。ここでは「10で 割る」ということは変わりませんし、途中で間違えて変えると困るものですから変えられないように定数にしたいので const キーワードを利用します。変数であるにもかかわらず定数にするときに使います。

さて、このプログラムはいかにも例外が発生しそうなプログラムです。1つは「0での除算」です。
数学において0で割ることはやってはいけないことであり、同時に答えもでません。プログラムにおいても0での除算はプログラマがコードを書いた場合は初歩 的なミスですが、ユーザは何をするかわかりません。わざわざ0を入力するユーザもいないとは言い切れません。

ユーザがコマンドライン引数で0を指定したときに例外が発生して、プログラムの制御が catch に移動します。catch はメソッドではなく、プログラムの制御自体が移動されます。
そのため「例外発生」を表示した後は } に行き着きますので問題なく終了します。

また、このプログラムにおいてもうひとつ発生しそうな例外があります。それはコマンドライン引数を指定しない場合です。
この場合、try の中の1番始めの行で、
args[0]
が null (参照なし)になるので例外が発生します。

catch を指定することで、try ブロック内で発生したすべての例外をキャッチできます。このプログラムを改良して、それぞれのエラーメッセージを表示できま す。
例外はすべて Exception クラスから派生し、Message という例外の内容を取得するプロパティが用意されています。それを利用して以下のように改良することができます。
using System;

class Class1
{
static void Main(string[] args)
{
try
{
int i = int.Parse(args[0]);
const int a = 10;
Console.WriteLine(a/i);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
例外情報は Exception クラス型の変数 ex に渡されます。ex.Message プロパティにアクセスすることでその内容を得られます。(ToString() されて文字列の値として返されるので、そのまま表示可能)

この方法を利用すれば、0で除算しようとしたとき、コマンドライン引数がない場合、それぞれ「0 で除算しようとしました。」「インデックスが配列の境界線です。」というメッ セージが出力されます。

特定の例外をキャッチする

catch を指定することですべての例外に対応できることは事実です。私は面倒なので上の手を利用して例外を回避していますが、実際本格的なプログラムにしようとし たら、それぞれの例外の種類によって処理を使い分けるべきです。

例外の種類は非常に多くあり、とてもすべてを説明できません。SDK ドキュメントを参照してください。
ここでは上の「0での除算」と「インデックスの境界線越え」という有名な2つの例外をキャッチし、例外をマスターしたいと思 います。
using System;

class Class1
{
static void Main(string[] args)
{
try
{
int i = int.Parse(args[0]);
const int a = 10;
Console.WriteLine(a/i);
}
catch(DivideByZeroException)
{
Console.WriteLine("0を入力するバカがあるか!!");
}
catch(IndexOutOfRangeException)
{
Console.WriteLine("コマンドライン引数を入力して実行せんかい!!");
}
}
}
特定の例外をキャッチするには catch に続けて、その例外の型を続けます。それぞれの例外はクラスとして用意されているので、クラス名を記述することになります。ここでは、
DivideByZeroException クラス
IndexOutOfRangeException クラス
が使われています。

DivideByZeroException クラスは
すべての例外 : Exception クラス
システム(.NET エンジン)に関する例外
: System.SystemException クラス
演算(算術・キャスト・変換)に関する例外
: System.ArithmeticException クラス
がこの順に派生されたクラスです。
DivideByZero というのは「0で割る」を意味します。0での除算が発生したときにこれが呼び出されます。

IndexOutOfRangeException クラスは
すべての例外 : Exception クラス
システム(.NET エンジン)に関する例外
: System.SystemException クラス
がこの順に派生されたクラスです。

IndexOutOfRange というのは「範囲を超えたインデックス」を意味し、配列に関する例外です。
例えばこのプログラムでコマンドライン引数を指定しない場合、args[0] がエラーとなります。

この2つの例外はこれまでの説明の通り、発生すると予想される場所は違います。
0での除算は
Console.WriteLine(a/i);
で発生し、インデックスの境界線越えは
int i = int.Parse(args[0]);
で発生します。

また、この単元の始めに述べた if で例外をキャッチするようにしたら、0での除算はキャッチすることができますが、インデックスの境界線越えはキャッチできません。
また、if でキャッチしようとすると、すべての if - else if のハシゴがチェックされて、初めて else に記述されたコードが実行されるため能率がかなり悪くなります。
例外は発生するまでは try の中を実行しますので、能率が下がることはありません。

上のサンプルを if で以下のように書いてはいけないのがお分かりいただけますか?
下のサンプルはインデックスの境界線越えをキャッチできず、コマンドライン引数が指定されないときには例外が発生し、以上終了します。能率もいかにも悪そ うです。
/*
このサンプルは悪い例です
*/

using System;

class Class1
{
static void Main(string[] args)
{
// args[0] の時点で例外が発生するので、真偽が出ない
if (args[0] == "0")
Console.WriteLine("0を入力するバカがあるか!!");
else if (args[0] == "")
Console.WriteLine("コマンドライン引数を入力して実行せんかい!!");
// 下が実行されるまで↑の else if をすべて行うので能率が悪い
else
{
int i = int.Parse(args[0]);
const int a = 10;
Console.WriteLine(a/i);
}
}
}

[ ステップアップC# ] [ C#プログラミング応用講義 ]