デリゲート
デリゲートとは
最後に、C#言語の最大の特徴の一つともいえる処理である、デリゲート(delegate)についての説明をします。デリゲートは、JavaやC++には、該当する概念がない独自の処理で、C#言語(というよりも、.NETフレームワーク)独自の処理です。
デリゲートは、日本語で、「移譲する」という意味で、他のクラスのメソッドを参照するオブジェクトのことを指し、主にイベント処理などに用いられる重要な概念です。
サンプルプログラム
では、実際にデリゲートを使った簡単なサンプルプログラムを実行してみましょう。
プロジェクト:SampleEx701/ファイル名Program.csusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SampleEx701 { // デリゲート delegate void Operation(int a,int b); // Calcクラス class Calc { public void Sub(int a, int b) { Console.WriteLine("{0} - {1} = {2}", a, b, a - b); } } // Programクラス class Program { static void Add(int a, int b) { Console.WriteLine("{0} + {1} = {2}", a, b, a + b); } static void Main(string[] args) { Calc c = new Calc(); // デリゲートの設定 Operation o1 = new Operation(Add); Operation o2 = new Operation(c.Sub); // デリゲートで設定したメソッドの呼び出し o1(2, 1); o2(2, 1); } } }
2 - 1 = 1
デリゲートの宣言
デリゲートを使うには、まずはデリゲートの宣言を行わなくてはなりません。デリゲートの宣言とは、以下のようになります
デリゲートの宣言これは、指定した名前のデリゲートを生成するというもので、ここで指定した同一の戻り値の型、そして引数をとるメソッドを、オブジェクトとして利用できるようになります。このサンプルの例で行くと、10行目の処理で、「Ooperatin」という名前で、引数がint型を二つ、戻り値としてvoidをとるメソッドを、オブジェクトとして持つことができるようになります。
処理の移譲と利用
では、このように宣言したメソッドを、どのように利用したらよいのでしょうか?そのために行うのは、まずはそのためのオブジェクトを生成することです。30行目、および31行目を見てください。o1は、引数として、このクラスのstaticなメソッドaddを、o2は、引数として、Calcクラスのインスタンスcのメソッドsubを与えています。
このように、引数として、同一の戻り値の型、および引数を持つメソッドの名前を引数として与えると、生成されたインスタンスo1およびo2は、それぞれaddメソッド、およびcのsubメソッドとしてふるまうことが可能になります。実際、33行目の処理で、Addメソッドが、34行目の処理でSubメソッドが呼び出されています。
デリゲートのオブジェクトの使用と、各メソッドの関係o1(2,1) | → | add(2,1) |
---|---|---|
o2(2,1) | → | c.sub(2,1) |
このように、デリゲートは、引数と戻り値の型が同じメソッドの役割を引き受けるオブジェクトを生成することができ、名前は違ってもあたかもそのメソッドを呼び出しているのと同じ結果を得ることが可能になります。(図7-1.参照)
図7-1.デリゲートの仕組み複数のメソッドのデリゲート
サンプルプログラム
デリゲートは、実は同時に複数のメソッドを同時に呼び出すことができます。以下のサンプルを実行してみてください。
プロジェクト:SampleEx702/ファイル名Program.csusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SampleEx702 { class Program { // デリゲートの宣言 delegate void Action(int a); // 1つ目の処理 static void Func1(int a) { Console.WriteLine("a={0}",a); } // 2つ目の処理 static void Func2(int a) { Console.WriteLine("a*2={0}", a * 2); } // 3つ目の処理 static void Func3(int a) { Console.WriteLine("a*3={0}", a * 3); } static void Main(string[] args) { // デリゲートaの作成 Action a = new Action(Func1); // 処理の追加 a += new Action(Func2); a += new Action(Func3); // 処理の実行 a(3); } } }
a*2=6
a*3=9
この処理では、12行目でActionという名前のデリゲートを宣言しています。31行目で、メソッドFunc1を移譲していますが、さらに、32行目、33行目で、「+=」演算子を用いて、これにら更にFunc2、Func3を加えています。この状態で、36行目で、引数3を与えて実行すると、Func1、Func2、Func3のそれぞれが、引数3を与えられた状態で実行されます。
このように、デリゲートには、同型であれば、複数のメソッドを追加できます。なお、実行順序は、追加された順になります。(図7-2.参照)
図7-2.複数のメソッドのデリゲート例外処理
例外とは
プログラムを実行中に発生する動作における予期せぬ事象(エラー)のことを、例外と言います。たとえば、0で割り算を行ったり、配列変数の範囲からはみ出してしまったりするようなものが、代表的な例外です。通常、例外が発生すると、その段階でプログラムは異常終了しますが、例外が発生した時点でそれを検知し、何らかの処理を行うことを、例外処理と言います。
例外処理で、もっとも基本的なものは、try~catchによる処理です。書式は以下のようになります。
try~catchの書式(処理①)
}catch((例外クラス) 変数){
(処理②)
}finally{
(処理③)
}
処理①の中で、エラーが出た場合、例外処理である、処理②が実行されます。また、処理③は、例外が発生する、しないに関係なく実行されます。なので、例外が発生する場合、処理①→処理②→処理③の順に処理が実行され、発生しない場合には、処理①→処理③となります。
たとえば、0での割り算が発生した場合、DivideByZeroExceptionという例外が発生します。その際、この例外をcatchするようにしておけば、それによりシステムが強制終了することを回避できます。(図7-3.)
図7-3.try/catchによる例外処理なお、例外の種類はたくさんあるため、try一つに対し、catchは複数用意することが可能です。ただ、finallyは、一つだけしか定義できません。しかし、必要がなければ、finallyを省略することは可能です。
例外クラス
ここで出てくる、DivideByZeroExceptionとは、例外クラスの一つで、0割り算による例外が発生したときにインスタンスが生成されます。例外クラスは、このほかにもさまざまな種類がありますが、いずれも、Exceptionクラスを継承しています。
複数の例外を受ける
サンプルプログラム
try/catchでは、複数の例外を受けることができます。まずは、以下のサンプルを実行してみてください。
プロジェクトSampleEx703/Program.csusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SampleEx703 { class Program { static void Main(string[] args) { try{ for(int i = 0; i<= 5; i++){ int a = getNum(i); int b = 5; Console.Write(a + " / " + b + " = "); Console.WriteLine(calc(a,b)); } }catch(DivideByZeroException e){ Console.WriteLine(); Console.WriteLine("0による割り算発生"); }catch(IndexOutOfRangeException e){ Console.WriteLine("配列の範囲外にアクセスしました"); }finally{ Console.WriteLine("終了"); } } // 計算処理(例外を発生させる) private static int calc(int a,int b){ return a / b; } // 範囲外に出たときの処理 public static int getNum(int index){ int[] num = { 1, 2, 3, 4 }; return num[index]; } } }
2 / 5 = 0
3 / 5 = 0
4 / 5 = 0
配列の範囲外にアクセスしました
終了
13行目から19行目のtryの中で、例外が起こった場合、catchに飛びます。このサンプルのようにcatchは複数定義できます。発生した例外の種類によって、処理を分岐させることができます。
30行目のcalcメソッド、および34行目のgetNumメソッドは、それぞれ処理の内部で、DivideByZeroExceptionおよび、IndexOutOfRangeExceptionを発生させる可能性があります。その例外は、呼び出し元のMain()メソッドの中のtry/catchによって、例外として処理されます。(図7-4.)
図7-4.メソッドの外で例外をキャッチ例学を発生させる
サンプルプログラム
なお、例外は必要に応じて、プログラマーが意図的に発生させることも可能です。以下のサンプルを実行してみてください。
プロジェクトSampleEx704/Program.csusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SampleEx704 { class Program { static int GetNum(int i) { int[] nums = { 300, 600, 900 }; if (i > nums.Length) { // 例外を発生させる throw new IndexOutOfRangeException(); } return nums[i]; } static void Main() { try { int result = GetNum(4); } catch (IndexOutOfRangeException e) { Console.WriteLine("配列の範囲外にアクセスしました。"); } } } }
このサンプルでは、GetNumメソッドにより、配列numsから、値を取得しています。引数としてインデックスが与えられますが、配列の成分は3つしかありませんから、4は明らかに範囲外です。そういったときに、17行目の処理で、例外IndexOutOfRangeExceptionを発生させています。これは、配列の範囲外にアクセスしたときに発生する例外ですが、ここではthrowを用いて意図的に発生させています。 throw
このようにすれば、その場で例外を発生させることができます。一方、Main()の側ではそれを受けて、catch以下の処理で、「配列の範囲外にアクセスしました。」と表示させています。
練習問題 : 問題7.