C#
 Computer >> コンピューター >  >> プログラミング >> C#

【C#入門】デリゲートの基本概念と使い方を徹底解説

C言語の経験者であれば、デリゲートを「関数ポインタのようなもの」と捉えることができます。しかし、C#のデリゲートは単純な関数ポインタをはるかに超えた機能を持っています。本記事では、デリゲートの基本的な概念と、日常的なプログラミングにおける活用方法について詳しく解説します。

本質的に、デリゲートは一種の「間接参照」の仕組みを提供します。デリゲートはコードの断片をカプセル化し、型安全な形での受け渡しや実行を可能にします。動作をその場ですぐに実行するのではなく、一旦オブジェクトの中に格納しておき、必要に応じてそのオブジェクトに対してさまざまな操作を行えます。その操作の一つが、格納された動作の実行です。

デリゲートを使うことで、「高階関数」——つまり、関数を引数として受け取ったり、戻り値として関数を返したりする関数——を記述できるようになります。デリゲート型は、そのデリゲートが表現できるメソッドのシグネチャ(戻り値の型とパラメータの型)を定義します。次の例では、Transformerは「整数を受け取り、整数を返す」任意のメソッドを表現できるデリゲートです。

delegate int Transformer(int x);

このシグネチャを満たすメソッド(ラムダ式、インスタンスメソッド、静的メソッドなど)であれば、どれでもTransformerのインスタンスに代入できます。例えば以下の通りです。

Transformer square = x => x * x;
Transformer cube = x => x * x * x;

Console.WriteLine(square(3)); // 9 を出力
Console.WriteLine(cube(5));   // 125 を出力

デリゲートはいつ使うべきか?

デリゲートは通常、「何らかのアクションを実行したいコードが、そのアクションの具体的な内容はわからないが、アクションのインターフェース(シグネチャ)はわかっている」という場面で使用されます。

プログラミングでは、特定の処理を実行する必要があるものの、実行時にどのメソッドを呼び出せばよいのか事前にはわからないという状況によく直面します。デリゲートは、その動作をデリゲートに置き換え、文脈や状況に応じて適切な動作を持つ具体的なインスタンスを渡すことで、この問題をエレガントに解決してくれます。

デリゲートが実際に何らかの処理を行うためには、次の4つのステップが必要です。

1) デリゲート型を宣言する

デリゲート型とは、それが表現する関数の定義そのものです。具体的には、関数が受け取るパラメータの型と、返す戻り値の型で構成されます。

例えば、「2つの数値を受け取り、数値を返すメソッド」を表すデリゲート型は次のように宣言できます。

delegate int Processor(int numOne, int numTwo);

Processorは、クラスから作成される型と同様に、それ自体が一つの型です。この型のインスタンスを作成するには、「2つの数値を受け取り、数値を返すメソッド」が必要になります。

2) 実行するコードをメソッドとして定義する

上記のデリゲート型と完全に同じシグネチャを持ち、実行時の状況に応じて望む処理を行うメソッドを定義します。例えば、以下のいずれのメソッドも「2つの数値を受け取り、数値を返す」という条件を満たすため、Processorのインスタンス作成に使用できます。

static int Add(int numOne, int numTwo){
    return numOne + numTwo;
}
static int Subtract(int numOne, int numTwo){
    return numOne - numTwo;
}

3) デリゲートのインスタンスを作成する

デリゲート型と、正しいシグネチャを持つメソッドが揃ったら、そのデリゲート型のインスタンスを作成できます。これにより、C#コンパイラに対して「デリゲートのインスタンスが呼び出されたときに、このメソッドを実行する」ということを伝えていることになります。

Processor processorOne = new Processor(Add);
Processor processorTwo = new Processor(Subtract);

上記の例では、AddメソッドとSubtractメソッドが、デリゲートのインスタンスを作成するのと同じクラス内で定義されていることを前提としています。もしメソッドが別のクラスで定義されている場合は、そのクラスのインスタンスが必要になります。

4) デリゲートのインスタンスを呼び出す

これは、デリゲートのインスタンスに対してInvokeという名前のメソッドを呼び出すだけです。このメソッドは、デリゲート型の宣言で指定されたものと同じパラメータリストと戻り値の型を持ちます。Invokeを呼び出すと、デリゲートのインスタンスが持つアクションが実行されます。

int sum = processorOne.Invoke(3, 5);

ただし、C#ではさらに簡単に記述できます。デリゲートのインスタンスは、それ自体がメソッドであるかのように直接呼び出すことが可能です。例えば、

int difference = processorTwo(10, 6);

デリゲートの結合と削除

デリゲートのインスタンスを1回呼び出すだけで、複数の異なるアクションを実行したい場合、C#ではそれが可能です。System.Delegate型には、CombineRemoveという2つの静的メソッドが用意されています。

1. Combine

パラメータとして渡されたデリゲートの呼び出しリスト(invocation list)を連結した、新しいデリゲートを作成します。新しいデリゲートのインスタンスが呼び出されると、含まれるすべてのアクションが順番に実行されます。

public static Delegate Combine(params Delegate[] delegates); // または
public static Delegate Combine(Delegate a, Delegate b);

なお、呼び出しリスト内のいずれかのアクションが例外をスローすると、それ以降のアクションは実行されなくなる点に注意してください。

2. Remove

あるデリゲートの呼び出しリストから、別のデリゲートの呼び出しリストの最後の出現箇所を削除します。具体的には、sourceの呼び出しリストからvalueの呼び出しリストの最後の出現を取り除いた、新しい呼び出しリストを持つデリゲートを返します。

public static Delegate Remove(Delegate source, Delegate value);

まとめ

  • デリゲートは、特定の型とパラメータのセットを持つ動作をカプセル化します。これは、メソッドを1つだけ持つインターフェースに似ています。

  • デリゲート型の宣言が示す型シグネチャによって、どのメソッドを使ってデリゲートのインスタンスを作成できるか、また呼び出し時のシグネチャが決まります。

  • デリゲートのインスタンスを作成するには、デリゲートが呼び出されたときに実行したいメソッドが必要です。

  • デリゲートのインスタンスは、文字列と同様に不変(イミュータブル)です。

  • 各デリゲートのインスタンスは、呼び出しリスト(アクションのリスト)を保持しています。

  • デリゲートのインスタンス同士は、結合したり削除したりすることができます。

サンプルコード全体

using System;
class Program{
    delegate int Transformer(int x);
    delegate int Processor(int numOne, int numTwo);
    static void Main(){
        Transformer square = x => x * x;
        Transformer cube = x => x * x * x;
        Console.WriteLine(square(3)); // 9 を出力
        Console.WriteLine(cube(5));   // 125 を出力
        Processor processorOne = new Processor(Add);
        Processor processorTwo = new Processor(Subtract);
        int sum = processorOne.Invoke(3, 5);
        Console.WriteLine(sum); // 8 を出力
        int difference = processorTwo(10, 6);
        Console.WriteLine(difference); // 4 を出力
    }
    static int Add(int numOne, int numTwo){
        return numOne + numTwo;
    }
    static int Subtract(int numOne, int numTwo){
        return numOne - numTwo;
    }
}

実行結果

9
125
8
4
  1. C言語のポインタ入門!宣言・初期化から配列アクセスの実例まで徹底解説

    ポインタ(pointer)とは、他の変数のメモリアドレスを格納するための変数です。C言語においてポインタは、メモリを直接操作したり、関数間でデータを効率的にやり取りしたりするために欠かせない重要な概念です。 ポインタの宣言・初期化とアクセス まず、次のような通常の整数変数の宣言を見てみましょう。 int qty = 179; 変数qtyには値「179」が格納されると同時に、メモリ上のどこかにその格納場所(アドレス)が割り当てられます。 ポインタの宣言 int *p; 「p」はポインタ変数であり、別の整数型(int)変数のアドレスを保持します。変数名の前にアスタリスク(*)を付けることで、「こ

  2. C#のクラスとは?基本概念からコンストラクタまで徹底解説

    C#におけるクラスは、最も重要な型の一つです。クラスは、問題領域に関連するオブジェクトを生成するための「設計図」と考えることができます。つまり、クラスはテンプレートであり、そこからオブジェクトを作成します。クラスには、そのクラスから作られるオブジェクト群が共有する構造(データ)と振る舞い(メソッド)が定義されています。たとえるなら、クラスは「クッキーの型」であり、オブジェクトはその型で作られた「クッキー」そのものです。 カプセル化という重要な概念 クラスは、オブジェクト指向プログラミングにおける重要な概念である「カプセル化」を実現します。カプセル化とは、データと、そのデータを操作する処理を一箇