よくわかってなかったcacaponがまとめたC#のデリゲートの作り方

二次元配列の各要素に対して、処理を行いたいとき、

簡単に思いつく形と言えば、こんな形になるのかなと思います。

for (int h = 0; h < height; h++)
{
    for (int w = 0; w < width; w++)
    {
        //何らかの処理
    }
}

一つだけなら、これの中に処理を入れればいいのですが、

その要素に対して行いたい処理が複数ある場合、どうしたらよいでしょう??


例えば、私はパズル要素があるゲームを作成中なのですが、

二次元配列に対して、以下の処理をしたいという要望がありました。

  • すべての配列の要素を、初期値で初期化したい。
  • パズルのピースデータを動かす用の配列にセットしたい。
  • 二つの同じ大きさの配列を比べて、重複していなかったら一方の配列に追加したい。
  • etc...

これすべてに前述の二重のfor文を記載するのも一つの手なのでしょうが、

同じものを何度も書くのはミスの始まりですし、何よりまとめられるならまとめたい。

そう思ったのです。


ただ、今回はInterfaceのように他の所から呼び出すわけじゃなく、

クラス内で完結しています。それに対するかき方ってあるのでしょうか?


と思い、調べたところ、デリゲートという機能を使うと実現できそうという事が分かりました。


今回は、そのデリゲートについて、cacaponなりに理解した内容をまとめてみたいと思います。

こんな感じで実装してみました。

デリゲートのテスト

説明

ココでは三つの関数を作成しました。

  • private void InitErea((Guid, ePieceColor)[,] erea)
  • private void Search(int height,int width, Action<int,int> action)
  • public void SetHoldErea((Guid,ePieceColor)[,] piece)

このうち、二次元配列の各要素を順番に見ていく部分をSearchとして定義し、

内部の処理に関しては、各呼び出し側で作成して、それを入れるような形で表現しています。

例えば、InitEreaでは erea[height, width] = (Guid.Empty, ePieceColor.NONE);という処理を行わせている感じですね。

ちょっと処理が分かりにくいと思うので、InitEreaの動きを見てみましょう。

  1. InitEreaを呼びだす。
  2. Searchを実行するために、引数として高さ、幅、行いたい処理を渡す。
    この時の中の処理が(height,width)=>{erea[height, width] = (Guid.Empty, ePieceColor.NONE);})です。
  3. Searchが実行され、二重のfor文の内部、各要素毎にaction(h,v);が呼ばれます。

2重のFor文で二次元配列にアクセスするイメージは、下記のような感じです。

f:id:cacapon:20210714182718p:plain

要素h0w0の時はaction(0,0)が呼ばれ、

つまり、(0,0)=>{erea[0, 0] = (Guid.Empty, ePieceColor.NONE);}の処理が行われる感じですね。

h1w3なら(1,3)=>{erea[1, 3] = (Guid.Empty, ePieceColor.NONE);}になります。

デリゲートの前にラムダ式について

ここで当たり前のように出てきた、 () => {何かの処理;}は慣れていないと分かりにくいですよね。

私もハマったところなのですが、これはラムダ式と言って、名前を定義しなくても使える関数になります。


例えば、上述の(height,width)=>{erea[height, width] = (Guid.Empty, ePieceColor.NONE);})

private void 名無しの関数(int height, int width)
{
    erea[height, width] = (Guid.Empty, ePieceColor.NONE);
}

というのと実質的には同じになるようです。

デリゲートでは、処理の内容をラムダ式で記載することが多いようで、

実質セットとして扱われています。

最近良く使われているのは下記の四つのデリゲートらしいです

C#で使われるデリゲートは何種類もあるらしいのですが、

最近使われているのは下記4種類が多いようです。

  • Action
     引数無し、戻り値なし
  • Action<引数1の型(,...引数Nの型)>
     引数有り、戻り値なし
  • Func<戻り値の型>
     引数無し、戻り値有り
  • Func<引数1の型(,...引数Nの型),戻り値の型>
     引数有り、戻り値有り

私自身も理解するうえで、上の形式が理解しやすかったため、

上記の定義方法だけ覚えています。


今回の例では、

private void Search(int height,int width, Action<int,int> action)

の三番目の引数が 引数有り、戻り値なしの関数として使いたかったため、

Action<int,int>型のデリゲート actionを使用しています。

まとめ

今回の二重ループの中身だけ処理を変えたい!

のような、途中まで同じ処理なのに、一部だけ変えたい場合、

デリゲートによる実装は便利だと思います。


引数や戻り値の有無で使えるデリゲートは変わりますが、

そこさえ分かれば、変数のように

関数を代入することができるようになるのは

実装の幅が広がるのではないかと私は思います。


こんかいはここまで、また会いましょう。