C#: 代表者とイベント

1. デリゲートに関する概念

C#におけるデリゲートとは、メソッドをパラメータとして渡すことを可能にする、型安全な関数ポインタのことです。デリゲートはメソッドのシグネチャ(戻り値の型とパラメータリスト)を定義しており、そのシグネチャに一致するメソッドのみをデリゲートインスタンスに割り当てることができます。

C/C++の関数ポインタとは異なり、デリゲートはオブジェクト指向であり、型安全で、マルチキャスト呼び出しをサポートしています。


2. デリゲートの宣言と使用

delegateというキーワードを使用してデリゲート型を宣言し、インスタンスを作成してメソッドに関連付けます。

▶ サンプル

CSHARP
delegate int MathOp(int a, int b);

class Program
{
    static int Add(int a, int b) => a + b;
    static int Multiply(int a, int b) => a * b;

    static void Main()
    {
        MathOp op = Add;
        Console.WriteLine(op(3, 4));

        op = Multiply;
        Console.WriteLine(op(3, 4));
    }
}
▶ 試してみよう
TEXT 📖 参照専用
7
12

3. マルチキャスト・デリゲート

デリゲートはマルチキャストに対応しており、1つのデリゲートインスタンスが複数のメソッドを参照できます。 +=を使用してメソッドを追加し、-=を使用してメソッドを削除します。呼び出しが行われると、すべてのメソッドは追加された順序で実行されます。戻り値を持つデリゲートの場合、マルチキャスト呼び出しでは最後のメソッドの結果のみが返されます。

▶ サンプル

CSHARP
delegate void NotifyHandler(string msg);

class Program
{
    static void LogToConsole(string msg) => Console.WriteLine($"Console: {msg}");
    static void LogToFile(string msg) => Console.WriteLine($"File: {msg}");
    static void LogToDb(string msg) => Console.WriteLine($"Database: {msg}");

    static void Main()
    {
        NotifyHandler handler = LogToConsole;
        handler += LogToFile;
        handler += LogToDb;

        handler("System started");

        Console.WriteLine("---");

        handler -= LogToFile;
        handler("System shutdown");
    }
}
▶ 試してみよう
TEXT 📖 参照専用
Console: System started
File: System started
Database: System started
---
Console: System shutdown
Database: System shutdown

4. 組み込みデリゲート

C# には、カスタム定義を行うことなくほとんどのシナリオに対応できる、よく使われる 3 つの組み込みデリゲートが用意されています。

(1) アクション

Actionは、戻り値のないメソッドを表します。0~16個のジェネリックパラメータをサポートしており、ActionAction<T>Action<T1,T2>といった形式になります。

▶ サンプル

CSHARP
class Program
{
    static void Main()
    {
        Action```<string>``` greet = name => Console.WriteLine($"Hello, {name}!");
        greet("Alice");

        Action noParam = () => Console.WriteLine("Parameterless Action");
        noParam();
    }
}
▶ 試してみよう
TEXT 📖 参照専用
Hello, Alice!
Parameterless Action

(2) Func

Func<T, TResult>は、戻り値を持つメソッドを表します。最後のジェネリックパラメータが戻り値の型であり、それ以前のものが入力パラメータとなります:Func<TResult>Func<T, TResult>Func<T1,T2,TResult>など。

▶ サンプル

CSHARP
class Program
{
    static void Main()
    {
        Func`<int, int, int>` add = (a, b) => a + b;
        Console.WriteLine(add(10, 20));

        Func```<string>``` getTime = () => DateTime.Now.ToString("HH:mm:ss");
        Console.WriteLine(getTime());
    }
}
▶ 試してみよう
TEXT 📖 参照専用
30
14:30:00

(3) 述語

Predicate<T>は、boolを返すメソッドであり、フィルタリングや条件チェックによく使用されます。

▶ サンプル

CSHARP
class Program
{
    static void Main()
    {
        Predicate```<int>``` isEven = n => n % 2 == 0;
        Console.WriteLine(isEven(4));
        Console.WriteLine(isEven(7));

        var numbers = new List```<int>``` { 1, 2, 3, 4, 5, 6 };
        var evens = numbers.FindAll(isEven);
        Console.WriteLine(string.Join(", ", evens));
    }
}
▶ 試してみよう
TEXT 📖 参照専用
True
False
2, 4, 6

5. 匿名メソッド

匿名メソッドでは、delegateキーワードを使用して、個別の名前付きメソッドを定義することなく、インラインのコードブロックを定義します。

▶ サンプル

CSHARP
class Program
{
    static void Main()
    {
        Func`<int, int>` square = delegate(int x) { return x * x; };
        Console.WriteLine(square(5));

        Action```<string>``` log = delegate(string msg) { Console.WriteLine($"[LOG] {msg}"); };
        log("Anonymous method example");
    }
}
▶ 試してみよう
TEXT 📖 参照専用
25
[LOG] Anonymous method example
💡 匿名メソッドはラムダ式の先駆けです。現代のC#開発では、より簡潔な構文を持つラムダ式 x => x * x が好まれています。


6. イベント

イベントとは、デリゲートに基づくパブリッシュ・サブスクライブの仕組みです。 デリゲートフィールドがeventキーワードで変更された場合、そのイベントを宣言しているクラスだけが内部的にそれを発生させることができます。外部のクラスは、+=および-=を使用して購読または購読解除を行うことしかできず、直接それを呼び出すことはできません。

▶ サンプル

CSHARP
class Button
{
    public event EventHandler```<string>``` OnClick;

    public void Click()
    {
        OnClick?.Invoke(this, "Button clicked");
    }
}

class Program
{
    static void Main()
    {
        var btn = new Button();
        btn.OnClick += (sender, msg) => Console.WriteLine($"Event received: {msg}");
        btn.Click();
        btn.Click();
    }
}
▶ 試してみよう
TEXT 📖 参照専用
Event received: Button clicked
Event received: Button clicked
⚠️ サブスクライバーが存在しない場合、OnClick.Invoke()を直接呼び出すとNullReferenceExceptionが発生するため、null条件付き呼び出しである?.Invoke()を使用する必要があります。


7. パブリッシュ・サブスクライブ・パターン

パブリッシュ・サブスクライブ(Publisher-Subscriber)パターンは、イベントの主要なアプリケーションシナリオです。パブリッシャーはイベントを発生させる役割を担い、サブスクライバーはそれに対応する役割を担います。この2つは完全に分離されています。

▶ サンプル

CSHARP 📖 参照専用
class NewsPublisher
{
    public event EventHandler```<string>``` OnNewsPublished;

    public void Publish(string news)
    {
        Console.WriteLine($"Publishing news: {news}");
        OnNewsPublished?.Invoke(this, news);
    }
}

class NewsSubscriber
{
    public string Name { get; }

    public NewsSubscriber(string name)
    {
        Name = name;
    }

    public void Subscribe(NewsPublisher publisher)
    {
        publisher.OnNewsPublished += HandleNews;
    }

    public void Unsubscribe(NewsPublisher publisher)
    {
        publisher.OnNewsPublished -= HandleNews;
    }

    private void HandleNews(object sender, string news)
    {
        Console.WriteLine($"  [{Name}] Received: {news}");
    }
}

class Program
{
    static void Main()
    {
        var publisher = new NewsPublisher();
        var sub1 = new NewsSubscriber("Reader A");
        var sub2 = new NewsSubscriber("Reader B");

        sub1.Subscribe(publisher);
        sub2.Subscribe(publisher);

        publisher.Publish("C# 12 officially released");

        sub2.Unsubscribe(publisher);
        publisher.Publish(".NET 9 performance improved by 50%");
    }
}
論理コード 43 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
Publishing news: C# 12 officially released
  [Reader A] Received: C# 12 officially released
  [Reader B] Received: C# 12 officially released
Publishing news: .NET 9 performance improved by 50%
  [Reader A] Received: .NET 9 performance improved by 50%

8. カスタム EventArgs

EventHandler<T>内のTは、通常EventArgsから継承され、カスタムイベントデータの伝達に使用されます。

▶ サンプル

CSHARP 📖 参照専用
class TemperatureEventArgs : EventArgs
{
    public double Temperature { get; }
    public DateTime Timestamp { get; }

    public TemperatureEventArgs(double temperature)
    {
        Temperature = temperature;
        Timestamp = DateTime.Now;
    }
}

class TemperatureMonitor
{
    public event EventHandler<TemperatureEventArgs> OnTemperatureAlert;
    private double _threshold;

    public TemperatureMonitor(double threshold)
    {
        _threshold = threshold;
    }

    public void CheckTemperature(double current)
    {
        Console.WriteLine($"Current temperature: {current}°C");
        if (current > _threshold)
        {
            var args = new TemperatureEventArgs(current);
            OnTemperatureAlert?.Invoke(this, args);
        }
    }
}

class Cooler
{
    public void StartCooling(object sender, TemperatureEventArgs e)
    {
        Console.WriteLine($"  🚨 Temperature exceeded! {e.Temperature}°C > threshold, starting cooling (time: {e.Timestamp:HH:mm:ss})");
    }
}

class Program
{
    static void Main()
    {
        var monitor = new TemperatureMonitor(30.0);
        var cooler = new Cooler();

        monitor.OnTemperatureAlert += cooler.StartCooling;

        monitor.CheckTemperature(25.0);
        monitor.CheckTemperature(32.5);
        monitor.CheckTemperature(28.0);
        monitor.CheckTemperature(35.0);
    }
}
論理コード 48 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
Current temperature: 25°C
Current temperature: 32.5°C
  🚨 Temperature exceeded! 32.5°C > threshold, starting cooling (time: 14:30:00)
Current temperature: 28°C
Current temperature: 35°C
  🚨 Temperature exceeded! 35°C > threshold, starting cooling (time: 14:30:01)

9. デリゲートとイベント

機能 デリゲート イベント
性質 型(関数ポインタ) 修飾子(制限付きデリゲートインスタンス)
外部からの呼び出し どこからでも呼び出し可能 宣言したクラス内でのみ内部的に発生させることができる
外部割り当て =で上書き可能 +=-=のみ使用可能
用途 コールバック、ストラテジーパターン パブリッシュ・サブスクライブ、通知メカニズム
安全性 低い 高い、呼び出し権限をカプセル化する
📌 イベントとは、本質的にevent修飾子が適用されたデリゲートであり、外部からのアクセスが呼び出しと代入に制限されることで、発行者のみがイベントを発生させられるよう保証されています。

❓ よくある質問

Q デリゲートとイベントの違いは何ですか?
A イベントとは、event修飾子が適用されたデリゲートです。外部コードからは、+=/-=を使用してのみ登録・解除が可能であり、直接呼び出したり上書きしたりすることはできません。また、内部的には宣言したクラスだけがイベントを発生させることができます。
Q 戻り値を持つマルチキャスト・デリゲートは何を返しますか?
A 最後に呼び出されたメソッドの戻り値を返します。それより前に呼び出されたメソッドの戻り値は破棄されます。
Q ActionとFuncの違いは何ですか?
A Actionには戻り値がありません(void)が、Funcには戻り値があります。最後のジェネリックパラメータが戻り値の型となります。
Q なぜイベントを発生させる際に?.Invoke()を使うのですか?
A イベントの購読者がいない場合にNullReferenceExceptionが発生するのを防ぐためです。?.Invoke()は、イベントがnullの場合、自動的に呼び出しをスキップします。
Q Predicate と Func<T, bool>は同等ですか?
A 機能的には同等です。Predicate<T>は、Func<T, bool>の特殊なバージョンであり、より明確な意味論を持ち、フィルタリングの場面で一般的に使用されます。

📖 まとめ

📝 練習問題

  1. delegate string Transform(string s);デリゲートを定義し、文字列を大文字に変換するメソッド、小文字に変換するメソッド、および文字列を逆順にするメソッドの3つを関連付け、マルチキャスト呼び出しをテストする
  2. Func<int, int, bool>を使用して、最初の数が2番目の数より大きいかどうかを判定する比較関数を実装する
  3. FileWatcherクラスを作成し、その中にevent EventHandler``<string>`` OnFileChangedを定義して、ファイルが変更された際にサブスクライバーへの通知をシミュレートする
  4. カスタムStockPriceEventArgs(銘柄コード、価格、変動率を含む)を定義し、変動率が閾値を超えた際にイベントを発生させる株価モニターを実装する
  5. 同じイベントを実装する際、カスタムデリゲートとEventHandler<T>の使用を比較し、それぞれの適用可能なシナリオをまとめなさい。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%