初学者にとってデリゲートの理解は大変だということで記事にしました。
対象は、VB, C#を勉強し始めて、デリゲートの構文は何となくわかるけど・・・・って人です
一部、説明のためにゴチャゴチャっとした部分を投げ捨てて簡単(にしすぎているかもしれませんが)にしています
#そもそもメモリ上では何が起きている?
よく、皆さんはこんなコードを書いていると思います
public void hoge()
{
//こんなんとか
sample fuga;
fuga = new sample();
//こんなの
sample fugafuga = new sample();
}
このコードでは何が起こっているのでしょう。
以下のコードを順に追っていきましょう
sample fuga; //1行目
fuga = new sample(); //2行目
###1行目で起きている事
プログラムが実行されると、生成されたデータはメモリ上に展開されます。
メモリは以下の図のような格子状の物をイメージしてください
さて、1行目のコードが実行されると、コンピュータはメモリから1マスを選び、そこを「fugaという変数の場所」として扱います
ちょうど、こんな感じです
そして、2行目ですが、これは以下の2つに分かれています
1.sampleクラスを作る
2.fugaにsampleクラスの場所を格納する
コレを順に追っていきましょう
sampleクラスを作るとき、それはメモリ上に作られます。クラスによって「メモリ上の領域を何マス使うか」は異なりますが、
今回、sampleクラスは5マス使うものとしましょう
b7番地にsampleクラスが作られて配置されました。
そして、「2.fugaにsampleクラスの場所を格納する」が行われると以下のようになります
つづいて、sampleクラスの入っているb7からの5マスをよく見てみます。
今回、sampleクラスは以下のようなクラスとします
class sample
{
public int a;
public string b;
public void hello()
{
Console.WriteLine("おはよう");
}
}
メモリ上の中身を眺めると、こんな風("風"です)になっているんです
この図を見ればわかるように、「メソッドも変数と同様にメモリ上に配置される」ということになるのです。
##メモリ上に配置されるということは?
ここまでの図をみると、
sample fuga; //1行目
fuga = new sample(); //2行目
この処理で記述されている「fuga」という変数は、単に、「メモリ上に実際に存在するsampleクラスのインスタンスの場所を持っているだけ」ということが理解していただけたかと思います。
変数は「メモリ上に存在する実際のクラスの場所を保存することが出来る」ということになります。
そして、**「変数はクラスと同様に、メソッドの場所も保存することが出来る」**のです。(超重要)
##メソッドの変数
さて、メソッドの場所を変数に保存できるということをお伝えしたわけですが、実はこれには名前がついています。
これこそが、「デリゲート」なのです!
デリゲートはメソッドを格納できる変数なのです!
##デリゲートのメモリ上の動き
メモリ上での動きを見てみます
public void hoge()
{
sample fuga; //1行目
fuga = new sample(); //2行目
sampleDelegate boo; //3行目
boo = new sampleDelegate(fuga.hello); //4行目
}
1行目と2行目はこれまでに見てきたものと同じなので省略し、3,4行目の処理が実行されるとどうなるのかを見ていきましょう
3行目の処理が実行されると、コンピュータはメモリから適当な1マスを選び、それを「booという変数の場所」とします
これが、「デリゲートはメソッドを入れることが出来る変数」という言葉の意味になります
(※これも読んでみてください http://gomocool.net/gomokulog/?p=250)
##メソッドの型
int型の変数に "おはようございます" が代入できないように、変数には**「型が違うものは代入できない(=それの場所を保持できない)」**というルールがありました。
a = "おはようございます"; //これはできない
実は、メソッドにも「型」が存在します。そして、メソッドの方は「引数と戻り値」で決定されるのです。
ですので、以下のようなコードは不可能になります
class sample
{
public int a;
public string b;
public void hello()
{
Console.WriteLine("おはよう");
}
}
private delegate void sampleDelegate(int a);
public void hoge()
{
sample fuga;
fuga = new sample();
sampleDelegate boo;
boo = new sampleDelegate(fuga.hello); //これは出来ない
}
なぜできないのか、それは「sampleDelegateというデリゲートは「int型を受け取り何も返さないメソッドの場所のみ格納できる」からです。
なので、sampleDelegateであるbooに「何も受け取らず何も返さないメソッドの場所を格納する」ことは出来ません
##どうしてそんな制約があるの?
「メソッドを入れることが出来る変数なのに、どうしてそんな制約を入れるのか?」という風に思った方もいるかと思います。
実は、この制約によって、あるメリットが発生します。
それを .netの「イベント」というものと合わせて説明します。
##そもそもイベントって?
イベントとは「マウスをクリックした」とか「ボタンを押した」とか「テキストボックスに入力した」とか
ユーザーの動作に対して名付けられたものです。
そして、.netではイベントとメソッドを紐づけることができ、紐づけられたメソッドはそのイベントが発生したときに呼び出されることになります。
このとき、メソッドを実行するために必要な引数は、イベント側からメソッドに渡すことになります。
この場合、イベント側からhoge, fuga に対してobject型とEventArgs型のデータを渡すことになります
##イベントに登録されたメソッドの型が違う時
もし、ここでボタンクリックイベントに登録されているメソッドの型が違った場合を考えましょう
こんな風に、イベント側に登録されたメソッドの引数の型が違うという事態になったとき、イベント側ではどうやっても対処できません。
しかし、同じ引数である場合は前項で示したように、呼び出すことが出来るようになります
(なぜなら、呼び出すメソッド全てが同じ引数を要求する場合、一つ作って、それを全部のメソッドにコピーして渡せばいいだけなので)
##いつ判断するか
さて、メソッドがどのような引数を受け取るかということは、「メソッドの型は何か」ということになりますので(正確には戻り値の型も含みます)、「イベントに登録されているメソッドの引数の型が違う」ということは、「イベントに登録されているメソッドの型が違う」ということと同じになります
このような事態が発生することを.netではいつ防いでいるのかということが問題となります。
考えられる可能性としては
・実際にイベントが発生して、データを渡す段階で問題発生!エラーで落ちる!(実行時エラー)
・コードを書いてる段階で、異なる型のメソッドが登録されている場合にエラーとなる(コンパイルエラー)
が挙げられます
.netでは、このうちの後者(コンパイルエラーとなる)を採用しているのです。
##どうやって判断するか
では、どのような方法で「イベントに登録されるメソッドの型を1つに絞り込んでいるのか」ということになります。
その方法にデリゲートを使っているのです。
イベントには「指定したデリゲートに代入できるメソッドしか登録できません」
具体的には、以下の図のような形です
このように、デリゲートを用いることで「イベントに登録できるメソッドに制約を与え」そのおかげでイベント側では呼び出されるメソッドに対して機械的に(まさに言葉通り!)同じデータを渡すだけで問題なく処理が進むことが保証されるわけです。
##見たことのある、あのコード
イベントを学習している時、コンなコードを見たのではないでしょうか?
public delegate void OriginEventHandler(int a);
public event OriginEventHandler hogeEvent;
このコードの意味は
1.「 int型を受け取り何も返さないメソッド」のみを代入できるデリゲートを作ります
2.hogeEventというイベントを作ります。このhogeEventに登録できるメソッドはOriginEventHandlerに代入できるメソッドだけです
という意味だったわけです。
##以上!