C# のデリゲートの基本

C# のデリゲートとは?

ボタンクリックなどのイベント処理では実行時に、システムがイベントを検出したときに、ユーザー定義のメソッドを呼び出して実行する必要があります。

通常はユーザー側のコードが API を呼び出すのに対して、システムがユーザーのコードを呼び出していることになります。こうした呼び出しを「コールバック」という言い方をします。

当然ながら、システム (フレームワーク) 側には,ユーザー定義のコードがあらかじめ埋め込まれているわけではありません。それにも関わらず、システムはユーザーのコードをコールバックすることができます。

なぜ、ユーザー定義の関数をシステム側からコールバックすることができるのでしょうか。

C# では、これから説明する「デリゲート」を利用することで、コールバックを実現することができます。

平たく言えば、デリゲートは関数のプレースホルダのようなものと考えることができます。関数の呼び出し箇所をデリゲートを使って定義しておき、実行時に実際に呼び出す関数への参照をセットすることで、コールバックを実現するのです。

C# のデリゲートの簡単な実装例

デリゲートはイベント処理などでよく使われます。 .NET でのイベント処理にまねた形で、サンプルコードを作ってみましょう。

ボタンをイメージして、 MyButton というクラスで、デリゲートを使っています。

using System;

class MyClickEventArgs : EventArgs
{
  public int x { get; set; }
  public int y { get; set; }

  public MyClickEventArgs(int x, int y)
  {
    this.x = x;
    this.y = y;
  }
}

class MyButton
{
  public delegate void MyEventHandler(EventArgs args);
  public MyEventHandler ClickHandler;

  public void Click()
  {
    // ダミーデータ
    var r = new Random();
    var x = r.Next(100);
    var y = r.Next(100);

    if (ClickHandler != null)
    {
      var args = new MyClickEventArgs(x, y);
      ClickHandler(args);
    }
  }
}

17行目で delegate 型を定義しています。

これによって、MyEventHandler という名前の delegate 型は 「EventArgs 型の引数をひとつ受け取り、戻り値はない (void である)」 ことになります。

18行目では MyEventHandler という delegate 型の変数 ClickHandler を宣言しています。 これにより、 ClickHandler という変数は、このコード内で「EventArgs 型の引数をひとつ受け取り、戻り値はない」関数として使えます。

ただし、その関数の実体は後から与えられるものであり null である可能性があります。このため、30行目で呼び出す前には 27行目で null のチェックをしています。

上でみたように MyEventHandler デリゲートは EventArgs 型の引数をとるように定義しています。

ここでは EventArgs をベースクラスにして、 MyClickEventArgs という型を定義して (3行目から13行目)、 30行目で呼び出す前に 29行目でMyClickEventArgsのインスタンスを作り、それを渡して ClickHandler を呼び出しています。

C# のデリゲートの簡単な利用例

上で作った MyButton を使う、簡単なサンプルコードを作りましょう。

using System;
using static System.Console;

class Program
{
  static void Main(string[] args)
  {
    var button = new MyButton();
    button.ClickHandler = OnClick;
    button.Click();
  }

  static void OnClick(EventArgs args)
  {
    var clickArgs = (MyClickEventArgs)args;
    WriteLine($"OnClick: ({clickArgs.x}, {clickArgs.y})");
  }
}

8行目で上で作成した MyButton オブジェクトを作成しています。

9行目で ClickHandler に 関数を設定しています。

MyButton の実装側では、 ClickHandler に関数が設定されていたらそれを呼び出す、というようなコードを書いていました。 このためここで OnClickClickHandler に設定することで、 MyButton 側から OnClick 関数がコールバックされることになります。

13行目の OnClick 関数では、パラメータは EventArgs 型で受け取りますが、 上で見たように MyButton 側では実際は EventArgs から派生した MyClickEventArgs を渡していました。 このため、ここでは argsMyClickEventArgs にキャストしています。

ダウンキャストになるので,用心深く as 演算子 を使うのも良いと思います。ここではコールバックする側される側の両方のコードが明確にわかっているので、単純にキャストしています。

10行目で MyButton クラスの Click() メソッドを呼び出しています。 Click() 関数ではデリゲートを使って、イベントハンドラ (を模した関数) のコールバックを行います。

このコードの実行結果は次のようになります。

OnClick: (21, 80)

xy の値は乱数で作ったダミーデータなので実行のたびに異なります。

この出力によって確かに OnClick() 関数が呼ばれ、 さらに引数も MyButton 側から正しく渡されていることが確認できます。

C# のデリゲートの設定箇所で何をしているのか

さて、全体の動作が確認できたところで少し詳しく見ていきましょう。

上のコードで、デリゲートに関数を設定する箇所は次のように書いていました。

var button = new MyButton();
button.ClickHandler = OnClick;
button.Click();

一方、デリゲートを定義する MyButton クラス側では、 ClickHandler は次のように、 MyEventHandler 型の変数として定義されていました。

public delegate void MyEventHandler(EventArgs args);
public MyEventHandler ClickHandler;

何故、変数に関数を「代入」することができたのでしょうか。

実はデリゲートの設定箇所は次のように書いたことと同じ意味になります。

var button = new MyButton();
button.ClickHandler = new MyButton.MyEventHandler(OnClick);
button.Click();

つまり、ClickHandler に関数を代入しているのではなく、 OnClick から作成されたデリゲートオブジェクトを設定しているのです。

C# ではデリゲートを設定する箇所を、このように、さも関数を代入するように簡略化できるのです。

このように他の書き方もできるものを簡潔にできるように,シンタックスでサポートされるものは一般に,シンタックスシュガー (糖衣構文) といいます。 C# 2.0 からこうした構文が書けるようになっています。

+= によるデリゲートの設定

デリゲートの設定は、次のように += 演算子を使って行う場合もあります。 =を使う場合と += を使う場合で,どのように違うのでしょうか。

MyButtonClick() メソッドでは次のように呼び出していました。

    if (ClickHandler != null)
    {
      var args = new MyClickEventArgs(x, y);
      ClickHandler(args);
    }

30行目では直接 ClickHandler 関数を呼び出す形になっていますが、実は直接呼び出すのではありません。

デリゲートは「インボケーションリスト」を持っています。

デリゲートに対して上の 30行目のように関数呼び出しを行うと,実際にはインボケーションリストに登録されたメソッドを次々と呼び出していきます。

デリゲートに関数を設定するときに次のように += を使うと、デリゲートのインボケーションリストに新しいデリゲートを追加することを意味します。

var button = new MyButton();
button.ClickHandler += OnClick;
button.Click();

このため、次のように二度デリゲートを追加すると、コールバックも二回発生します。

var button = new MyButton();
button.ClickHandler += OnClick;
button.ClickHandler += OnClick;
button.Click();

実行すると次のように二回 OnClick() 関数が呼び出されることが確認できます。

OnClick: (0, 70)
OnClick: (0, 70)

デリゲートの += による設定の別の書き方

上でデリゲートに関数を直接代入する形式は、C# 2.0 からのシンタックスシュガーであることを説明しました。

var button = new MyButton();
button.ClickHandler = new MyButton.MyEventHandler(OnClick);
button.Click();

+= を使った場合には、この部分はどのように変わるのでしょうか。

var button = new MyButton();
button.ClickHandler += OnClick;
button.Click();

+= を使ってデリゲートを設定した箇所は、 Delegate.Combine() メソッドを使って、インボケーションリストにデリゲートを追加することと同様になります。

var button = new MyButton();
button.ClickHandler = (MyButton.MyEventHandler)Delegate.Combine(
  button.ClickHandler,
  new MyButton.MyEventHandler(OnClick));
button.Click();

尚、 -= とすると、インボケーションリストの中から同じ型のデリゲートが、一つ削除されます。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 C# 入門