ぐっちーの駄弁り部屋

個人的に制作しているものの進捗や日常について不定期投稿

C#のデリゲート周りについてまとめておく

皆さんこんにちはぐっちーです。 緊急事態宣言下で私は4/29~5/11の期間がGWになって最初のほうがウハウハだったのですが外へ出てもお店の大半は営業していないので結局家から出ることなくゲームするか寝るかしかない生活を送っております。
そんな状態なので積読の消化を始めたんですがEffectiveC# 6.0/7.0という本を読んでる中でデリゲート周りのもやもやが晴れた気がするので忘れないうちに記事にしておきたいと思います。

はじめに

私はこれまでデリゲートがどんなものかくらいには知っていたんですがイベントとの違いを述べろだとかLinqなどでよく出てくるFuncやActionといったものが何なのかイマイチわかっていなかったのでそれらを使うことは基本的に避けていました。それが今回なんとなく解消されたというお話です。

デリゲートとは

デリゲートとは簡単に行ってしまえばメソッドを参照するためのです。そう型なのです。 型なので使用する際にはクラスや構造体と同じように変数を用意してそれに対してこのメソッドを参照してくれみたいに使います。
デリゲート型の変数を定義するには以下のようにメソッドの返り値と引数を指定します

delegate 返り値 型名(引数のリスト)
例
delegate void MyDelegate(int a);

そしてこのように定義したデリゲートにメソッドを登録したりデリゲートを介してそれらのメソッドを呼び出す場合には以下のように記述します。(クラスの前にデリゲートの定義があると思ってください)

class MyClass{
  static void Main(){
    //MyDelegate型の変数を用意してメソッドを追加
    Mydelegate del = Method1;

    del += Method2;
    del += Method3;

    del(3);
  }
  
  static void Method1(int a) {Console.WriteLine($"入力された数字は{a}です")};
  static void Method2(int a) {Console.WriteLine($"ニュウリョクサレタスウジハ{a}デス")};
  static void Method3(int a) {Console.WriteLine($"Input is {a}.")};
}

出力は以下の通りになります。

入力された数字は3です
ニュウリョクサレタスウジハ3デス
Input is 3.

さらっとコードに登場しましたがデリゲートには複数のメソッドを追加できます。デリゲート型のオブジェクトは参照するメソッドのリストを持っているのでそれを順に実行していくことになります。 また、追加するメソッドはstaticメソッドの他にもインスタンスメソッド(非staticメソッド)も追加できます。細かい使い方については以下のサイトに非常に丁寧に記載されているのでそちらを参照してください。 ufcpp.net

Predicate・Action・Func

C#にはPredicate、Action、Funcと呼ばれるものが存在します。これらは何なんでしょうか。それぞれが別々の役割を持ちますが共通して言えることはどれもジェネリック型を引数や返り値に使用するデリゲート型であるということです。

Predicate

Predicateは任意の個数の引数を取り返り値がBoolean型であるデリゲート型です。
つまるところ以下のように定義しても同じものができます。

delegate Boolean MyPredicate<T>(T val);

けどまあ事前に用意されているのであればわざわざ用意する必要がないのでPredicate型のデリゲートを使えばいいと思います。
ListにはFind()やRemoveAll()といったPredicate型を引数にとりそれがtrueを返すならそれぞれ処理を行うといったメソッドがあります。これらを使うときは直接引数にラムダ式で判定式を記述することが多いですがPredicate型の変数を指定することができます。

Action

Actionは任意の個数の引数を取り返り値がない(返り値がvoid)であるデリゲート型です。自分で定義するとこんな感じです。

delegate void MyAction<T>(T val);

Action型や後述するFunc型は引数を取らない場合もあります詳しくは公式のリファレンスなどを参照してください

docs.microsoft.com

Func

Funcは任意の個数の引数を取り返り値があるデリゲート型です。実装するとこんな感じ

delegate TResult MyFunc<T>(T, TResult)

TResult型についてはジェネリックな返り値の型だと考えてもらえればいいと思います。
LINQで出てくるメソッドはほぼ引数がFunc型です。LINQを使っている方でこう思う人がいるかも知れません
「いや、引数部分にはラムダ式を書いているのであってデリゲートなんてものは使ってないよ」と
そのラムダ式ですが実のところ記述することでデリゲートを生成しているのです。そもそもラムダ式とは匿名関数と呼ばれるものの一種であり、その匿名関数を用いることでインラインでデリゲート型のインスタンスを生成できるようになるのです。
そのためLINQの場合ではメソッドの引数部分にラムダ式を記述することでFunc型のデリゲートを生成して渡しているといった処理になるわけです。

event

デリゲートに似ているといえばeventですね。デリゲートが直接絡んでくるのはeventではなくてEventHandlerの方ではあるのですがそのあたりもまとめて話していきます。
まずeventというのはデリゲート型のプロパティの一種です。プロパティと何が違うかというとeventキーワードを付けた場合そのデリゲートに対して外部からはメソッドの登録と解除しかできなくなります。

public class MyEventArgs{
  public int a;
  public int b;
  public MyEventArgs(int i, int j) { a = i; b = j;}
}

public class MyClass{
  //通常のプロパティ
  private Action<object, MyEventArgs> m_myDelegate;
  public Action<object, MyEventArgs> MyDelegateProp {
    get{ 
      return m_myDelegate;
    } 
    set{
      m_myDelegate = value;
    }
  }
  //イベント
  private event Action<object, MyActionArgs> m_myEventHandler;
  public event Action<object, MyActionArgs> MyEventHandler{
    add {
      m_myEventHandler += value;
    }
    remove {
      m_myEventHandler -= value;
    }
  } 
}

こんな感じであったときにできる操作とできない操作の比較は以下のようになります。

public void Hoge(MyClass source){
  //プロパティへの操作(全部できる)
  source.MyDelegateProp = this.Method1;//メソッドの設定
  source.MyDelegateProp += this.Method1;//メソッドの追加
  source.MyDelegateProp -= this.Method1; //メソッドの解除
  source.MyDelegateProp = null;   //全解除
  source.MyDelegateProp(this, new MyEventArgs(1,2)); //デリゲートの呼び出しf 
  //イベントへの操作 
  source.MyEventHandler += this.Method1;  //メソッドの追加(できる)
  source.MyEventHandler -= this.Method2;  //メソッドの解除(できる)
  //source.MyEventHandler = this.Method1; メソッドの設定(できない)
  //source.MyEventHandler = null;   全解除(できない)
  //source.MyEventHandler(this, new MyEventArgs(1,2)); デリゲートの呼び出し(できない)
}

eventを使う場合それに登録されたメソッドの実行はeventが宣言されているクラス(上の例だとMyClass)内でしかできません。MyClass内ではプロパティ同様に使用することができます。
なのでObserverパターンのような使い方をしたい場合はEventを使ってメソッドを述語として使用したい場合は普通のプロパティを使えばいい感じです。

ざっくりまとめる

では最後に今回のまとめのまとめ(?)をしていきます。

  • デリゲートはメソッドへの参照を持たせるための型
  • Predicate・Action・Funcなどは事前に用意されたデリゲート型の一種
  • eventはデリゲート型のプロパティの一種で安全性が高い

こんな感じでしょうか。もう少し込み入った話は参考にした記事を貼っておきますのでそちらを見ていただければと思います。
個人的にだいぶこのあたりについて知識が深まった感じがするので改めてLINQとかを学び直そうかと思います。
それでは今日はこのへんで( ー`дー´)キリッ

参考記事

ufcpp.net

techtipshoge.blogspot.com

qiita.com

csharp.keicode.com

takap-tech.com