PR

[C#]イベントの使い方 -デリゲートと何が違う?-

今回はイベントについてです。

スポンサーリンク

前提知識

この記事でイベントについて説明するのに、「デリゲート」と「ラムダ式」が出てきます。
わかる人は飛ばしてください。ざっくり説明すると、
「デリゲート」は型の1つで、デリゲート型にはメソッドを入れることができる。
「ラムダ式」は使い捨てのメソッドを作ることのできる書き方。矢印みたいの「=>」が出てきたらラムダ式。
詳細はこちらの記事にまとめてあります。

[C#] デリゲートとラムダ式について
今回はデリゲート(delegate)とラムダ式についてです。イベントやLINQを使おうとすると出てくる用語ですね。 この2つがどういう関係かというと、「デリゲート型の変数や引数にラムダ式を使って値を代入する」ということをします。 デリゲート...
[C# 入門] 匿名関数(ラムダ式)の使い道
ラムダ式を使うと名前のないメソッド(匿名関数)を書くことができます。どういうときに使うのかというと、 イベントに登録するメソッドを書くときに使うタスク(非同期処理)に登録するメソッドを書くときに使うLINQのSelect、Whereメソッド...
スポンサーリンク

イベントとは?

WPFとかのGUIアプリケーションを作るときに、画面にボタンを置いたら、
ボタンがクリックされた時に呼ばれるメソッドに処理を書いて、とやっていると思います。
この「ボタンがクリックされた」時にメソッドが呼ばれるというのがイベントの機能です。
つまりイベントとは「オブジェクトの状態に変化があったことを通知する」機能です。

//ボタンがクリックされた時に呼ばれるメソッド
private void button_Click(object sender, RoutedEventArgs e)
{
    //ボタンがクリックされた時に行う処理を書く
}

GUIアプリケーションで「ボタンがクリックされた」、「画面の大きさが変わった」などのアクションのことをイベントといいます。そしてそのイベントが発生するとイベントハンドラに登録されたメソッドが呼び出されます(コールバックとか呼ばれる)。

「登録」と書いたけど公式ドキュメントにはSubscribeと書いてあるので「購読」とかが正しいかも?ですがこの記事では「登録」で統一してます。

VisualStudioでコードを書いている時は自動でコールバックされるメソッドを作ってくれるのであまり意識しないですが、試しにWPFのボタンクリックのイベントハンドラを見てみます。

public abstract class ButtonBase : ContentControl, ICommandSource
{
    public event RoutedEventHandler Click { .... }
}

eventと付いているのがイベントハンドラです。イベントハンドラはクラスのメンバーとして定義します。これはClickという名前で、RoutedEventHandlerという定義のメソッドが登録できるということになります。
RoutedEventHanderの定義を見てみると、

public delegate void RoutedEventHandler(object sender, RoutedEventArgs e);

ここでデリゲート(delegate)がでてきます。デリゲートはメソッドそのもの(正確には参照)を入れられる型なので、RoutedEventHandlerというデリゲート型の定義は、戻りなし(void)で引数にobject型のパラメータとRoutedEventArgs型のパラメータを持ったメソッドが入れられるということになります。

第1引数のobjectにはそのイベントが発生したコントロールそのものが渡されます。(ボタンクリックならボタンクラス)
第2引数はそのイベントに応じた情報が入ったオブジェクトが渡されます。(EventArgクラスから派生している)
C#で用意されているイベントハンドラは大体この形式のメソッドが登録できるようになっています。
この形式は.NETのガイドラインに準拠した書き方らしい、興味のある方はこちら

イベントのデザイン - Framework Design Guidelines
詳細情報: イベントのデザイン


ということで、イベントはデリゲートの機能を使って実現されています。
イベントとデリゲートの違いは外部からアクセスできるかになります。イベントは内部からのみデリゲートは外部からもアクセスが可能です。イベントに対して外部から出来るのはメソッドの登録と解除になります。

スポンサーリンク

イベントハンドラにメソッドを登録する

手動でイベントハンドラにコールバックされるメソッドを登録するには、
「イベントハンドラ += 登録するメソッド」という感じに書きます。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        //手動でボタンクリックのイベントハンドラにメソッドを登録
        this.button.Click += button_Click;
    }

    //ボタンがクリックされた時に呼ばれるメソッド
    private void button_Click(object sender, RoutedEventArgs e)
    {
        //ボタンがクリックされた時に行う処理を書く
    }
}

ラムダ式でメソッドを指定することもできます。この場合、解除するのが少しややこしくなるため解除もしたい時はちゃんとメソッドを定義しましょう。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        //手動でボタンクリックのイベントハンドラにメソッドを登録
        this.button.Click += (sender, e) => { };
    }
}

また、イベントハンドラ(というかデリゲート)には複数のメソッドを登録することができます。(マルチキャストという)

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        //手動でボタンクリックのイベントハンドラにメソッドを登録
        this.button.Click += (sender, e) => { };
        this.button.Click += (sender, e) => { };
        this.button.Click += (sender, e) => { };
    }
}


ちなみにWPFでコールバックされるメソッドを自動で作成した場合も裏でメソッドがイベントハンドラに登録されています。プロジェクトをビルドするときにできるobjフォルダの中の「*.g.i.cs」というファイルにイベントハンドラの登録が書いてあります。

this.button.Click += new System.Windows.RoutedEventHandler(this.button_Click);
スポンサーリンク

イベントハンドラに登録したメソッドを解除する

イベントハンドラに登録したメソッドを解除するには、
「イベントハンドラ -= 登録したメソッド」という感じに書きます。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        //手動でボタンクリックのイベントハンドラにメソッドを登録
        this.button.Click += button_Click;
        //イベントハンドラに登録したメソッドを解除
        this.button.Click -= button_Click;
    }

    //ボタンがクリックされた時に呼ばれるメソッド
    private void button_Click(object sender, RoutedEventArgs e)
    {
        //ボタンがクリックされた時に行う処理を書く
    }
}

ラムダ式でメソッドを登録した場合は、いったん変数にメソッドをいれてその変数使って登録と解除に使います。

        var callback = (object sender, EventArgs e) => { };
        test.myEvent += callback;
        test.myEvent -= callback;

イベントハンドラにメソッドを登録したクラスが使われなくなった(破棄する)時はイベントハンドラからメソッドを解除しないとメソッドへの参照が残ったままになってしまいます。プログラム実行中に登録側のクラスを頻繁に破棄する時は忘れずにメソッドを解除するようにしましょう。

スポンサーリンク

イベントを発生させるには?

上にも書きましたが、イベントを発生(イベントハンドラに登録されているメソッドを呼び出すことができる)のは、そのイベントハンドラが定義されているクラス内部からのみになります。
なので例えばボタンのクリックイベントを外部から発生させることはできません。自由にイベントを発生させることができるのは自分でイベントハンドラを持ったクラスを作ったとき(後述)になります。

ちなみに、各コントロールにはイベントを発生させるメソッドを持っている場合がある(メソッド名の先頭がOnイベント名になっているやつ)。大体protectedになってるので派生クラスを作ってこれを呼ぶことでイベントを発生させることが出来る。
Buttonクラスを見てみるとOnClickメソッドが定義されている。

public class Button : ButtonBase
{
    //-- 省略
    protected override void OnClick() { }
    //-- 省略
}

イベントを自作する

イベントを自作する方法を紹介。
まずイベントハンドラをクラスのメンバーとして定義します。
構文は「public event デリゲート型 イベントハンドラ名;」という感じです。

class TestClass
{
    //デリゲートの定義(引数1がobject型、引数2がint型のメソッドが登録できる)
    public delegate void TestEventArgs(object sender, int value);
    //イベントハンドラ
    public event TestEventArgs TestEvent;
}

delegete定義のところはActionを使ってdelegeteの定義を簡略化できます。

class TestClass
{
    //イベントハンドラ
    public event Action<object, int> TestEvent;
}

で、イベントを発生させるには「イベントハンドラ?.Invoke( … );」という感じにします。
メソッドが1つも登録されてないとイベントハンドラはnullになっているので「null条件演算子 ?」を使ってnullじゃなかったらというふうにします。
また、Invokeメソッドの引数には登録されているメソッドに渡す引数を指定します。
以下のサンプルはプロパティがSetterで変更されたらイベントを発生させています。

class TestClass
{
    //イベントハンドラ
    public event Action<object, int> TestEvent;

    private int _x;

    //プロパティ
    public int X { 
        get => _x;
        set {
            _x = value;
            //プロパティの値が変更されたらイベントを発生させる
            TestEvent?.Invoke(this, value);
        }
    }
}

自作したイベントハンドラを持ったクラスを使ってみます、画面作るの面倒なのでコンソールアプリです。

namespace EventTest;
class TestClass
{
    //イベントハンドラ
    public event Action<object, int> TestEvent;

    private int _x;
    public int X { 
        get => _x;
        set {
            _x = value;
            //プロパティの値が変更されたらイベントを発生させる
            TestEvent?.Invoke(this, value);
        }
    }
}

class Program
{
    static void Main()
    {
        var Test = new TestClass();

        //イベントハンドラにメソッドを登録
        Test.TestEvent += (sender, value) => Console.WriteLine($"Xに{value}が設定されました。");

        //プロパティの値を変更
        Test.X = 10;
    }
}
Xに10が設定されました。

イベントとデリゲートの違い

上の説明でイベントとデリゲートの違いは外部からアクセスできるかと書きましたが試してみます。

namespace EventTest;
class TestClass
{
    //デリゲート
    public Action TestDelegate;
    //イベントハンドラ
    public event Action TestEvent;

    //イベントハンドラを実行するメソッド
    public void RaiseEvent() {
        TestEvent?.Invoke();
    }
}

class Program
{
    static void Main()
    {
        var test = new TestClass();

        //---- デリゲート ----
        //デリゲートに直接メソッドを代入
        test.TestDelegate = () => Console.WriteLine("デリゲートに直接代入したメソッドです。");

        //デリゲートにメソッドを登録
        test.TestDelegate += () => Console.WriteLine("デリゲートに登録したメソッドです。");

        //外部からデリゲートにアクセス
        test.TestDelegate?.Invoke();

        //---- イベント ----
        //イベントハンドラに直接メソッドを代入(エラーになるのでコメントアウト)
        //test.TestEvent = () => Console.WriteLine("イベントハンドラに直接代入したメソッドです。");

        //イベントハンドラにメソッドを登録
        test.TestEvent += () => Console.WriteLine("イベントハンドラに登録したメソッドです。");

        //外部からイベントハンドラにアクセス(エラーになるのでコメントアウト)
        //test.TestEvent?.Invoke();

        //仕方ないのでイベントハンドラを実行するメソッドを呼ぶ
        test.RaiseEvent();
    }
}

と、こんな感じにイベントハンドラは外部からはメソッドの登録と解除しかできません。

デリゲートに直接代入したメソッドです。
デリゲートに登録したメソッドです。
イベントハンドラに登録したメソッドです。

C# 記事まとめページに戻る(他のサンプルコードもこちら)

C# プログラミング講座
C#についての記事まとめページです。開発環境VisualStudioのインストール方法や使い方、プログラミングの基礎知識についてや用語説明の記事一覧になっています。講座の記事にはすぐに実行できるようにサンプルコードを載せています。

コメント