C#: 代表者とイベント
1. デリゲートに関する概念
C#におけるデリゲートとは、メソッドをパラメータとして渡すことを可能にする、型安全な関数ポインタのことです。デリゲートはメソッドのシグネチャ(戻り値の型とパラメータリスト)を定義しており、そのシグネチャに一致するメソッドのみをデリゲートインスタンスに割り当てることができます。
C/C++の関数ポインタとは異なり、デリゲートはオブジェクト指向であり、型安全で、マルチキャスト呼び出しをサポートしています。
2. デリゲートの宣言と使用
delegateというキーワードを使用してデリゲート型を宣言し、インスタンスを作成してメソッドに関連付けます。
▶ サンプル
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));
}
}
7
12
3. マルチキャスト・デリゲート
デリゲートはマルチキャストに対応しており、1つのデリゲートインスタンスが複数のメソッドを参照できます。 +=を使用してメソッドを追加し、-=を使用してメソッドを削除します。呼び出しが行われると、すべてのメソッドは追加された順序で実行されます。戻り値を持つデリゲートの場合、マルチキャスト呼び出しでは最後のメソッドの結果のみが返されます。
▶ サンプル
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");
}
}
Console: System started
File: System started
Database: System started
---
Console: System shutdown
Database: System shutdown
4. 組み込みデリゲート
C# には、カスタム定義を行うことなくほとんどのシナリオに対応できる、よく使われる 3 つの組み込みデリゲートが用意されています。
(1) アクション
Actionは、戻り値のないメソッドを表します。0~16個のジェネリックパラメータをサポートしており、Action、Action<T>、Action<T1,T2>といった形式になります。
▶ サンプル
class Program
{
static void Main()
{
Action```<string>``` greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice");
Action noParam = () => Console.WriteLine("Parameterless Action");
noParam();
}
}
Hello, Alice!
Parameterless Action
(2) Func
Func<T, TResult>は、戻り値を持つメソッドを表します。最後のジェネリックパラメータが戻り値の型であり、それ以前のものが入力パラメータとなります:Func<TResult>、Func<T, TResult>、Func<T1,T2,TResult>など。
▶ サンプル
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());
}
}
30
14:30:00
(3) 述語
Predicate<T>は、boolを返すメソッドであり、フィルタリングや条件チェックによく使用されます。
▶ サンプル
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));
}
}
True
False
2, 4, 6
5. 匿名メソッド
匿名メソッドでは、delegateキーワードを使用して、個別の名前付きメソッドを定義することなく、インラインのコードブロックを定義します。
▶ サンプル
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");
}
}
25
[LOG] Anonymous method example
x => x * x が好まれています。
6. イベント
イベントとは、デリゲートに基づくパブリッシュ・サブスクライブの仕組みです。 デリゲートフィールドがeventキーワードで変更された場合、そのイベントを宣言しているクラスだけが内部的にそれを発生させることができます。外部のクラスは、+=および-=を使用して購読または購読解除を行うことしかできず、直接それを呼び出すことはできません。
▶ サンプル
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();
}
}
Event received: Button clicked
Event received: Button clicked
OnClick.Invoke()を直接呼び出すとNullReferenceExceptionが発生するため、null条件付き呼び出しである?.Invoke()を使用する必要があります。
7. パブリッシュ・サブスクライブ・パターン
パブリッシュ・サブスクライブ(Publisher-Subscriber)パターンは、イベントの主要なアプリケーションシナリオです。パブリッシャーはイベントを発生させる役割を担い、サブスクライバーはそれに対応する役割を担います。この2つは完全に分離されています。
▶ サンプル
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%");
}
}
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から継承され、カスタムイベントデータの伝達に使用されます。
▶ サンプル
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);
}
}
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修飾子が適用されたデリゲートであり、外部からのアクセスが呼び出しと代入に制限されることで、発行者のみがイベントを発生させられるよう保証されています。
❓ よくある質問
event修飾子が適用されたデリゲートです。外部コードからは、+=/-=を使用してのみ登録・解除が可能であり、直接呼び出したり上書きしたりすることはできません。また、内部的には宣言したクラスだけがイベントを発生させることができます。?.Invoke()を使うのですか?NullReferenceExceptionが発生するのを防ぐためです。?.Invoke()は、イベントがnullの場合、自動的に呼び出しをスキップします。<T, bool>は同等ですか?Predicate<T>は、Func<T, bool>の特殊なバージョンであり、より明確な意味論を持ち、フィルタリングの場面で一般的に使用されます。📖 まとめ
- デリゲートは、メソッドのシグネチャを定義する型安全な関数ポインタです
delegateというキーワードを使用して、カスタムデリゲート型を宣言します- マルチキャストデリゲートは、
+=/-=を介して複数のメソッドを結合し、それらを順番に実行します Actionには戻り値がなく、Funcには戻り値があり、Predicateはbool型を返す- 匿名メソッドでは、インライン定義に
delegate{ }を使用しますが、ラムダ式の方がより簡潔な代替手段となります - キーワード
eventは、デリゲートの呼び出しおよび代入の権限を制限し、宣言したクラスだけがそれを発生させることができるようにします - パブリッシュ・サブスクライブ・パターンでは、パブリッシャーとサブスクライバーが分離される
- カスタム EventArgs は、イベントデータを保持するために EventArgs 基底クラスを継承します
📝 練習問題
delegate string Transform(string s);デリゲートを定義し、文字列を大文字に変換するメソッド、小文字に変換するメソッド、および文字列を逆順にするメソッドの3つを関連付け、マルチキャスト呼び出しをテストするFunc<int, int, bool>を使用して、最初の数が2番目の数より大きいかどうかを判定する比較関数を実装するFileWatcherクラスを作成し、その中にevent EventHandler``<string>`` OnFileChangedを定義して、ファイルが変更された際にサブスクライバーへの通知をシミュレートする- カスタム
StockPriceEventArgs(銘柄コード、価格、変動率を含む)を定義し、変動率が閾値を超えた際にイベントを発生させる株価モニターを実装する - 同じイベントを実装する際、カスタムデリゲートと
EventHandler<T>の使用を比較し、それぞれの適用可能なシナリオをまとめなさい。