前回は、デリゲート型の基本を見ました。
今回は、デリゲートを使ってオブジェクトの状態変化を外部に通知する方法を整理します。
デリゲートは、単にメソッドを変数に入れるだけでなく、「何かが起きたときに、登録されている処理を呼び出す」ためにも使えます。
この考え方は、C# のイベントにもつながります。
状態変化を通知する
たとえば、温度センサーの値が変わったときに、外部の処理へ通知したいとします。
まずは、温度が変わったときに呼び出すデリゲート型を定義します。
delegate void TemperatureChangedHandler(int temperature);
このデリゲートは、変更後の温度を int で受け取るメソッドを参照できます。
次に、温度センサーのクラスを作ります。
using System;
delegate void TemperatureChangedHandler(int temperature);
class TemperatureSensor
{
public TemperatureChangedHandler? TemperatureChanged;
private int _temperature;
public void SetTemperature(int temperature)
{
_temperature = temperature;
TemperatureChanged?.Invoke(_temperature);
}
}
class Program
{
static void Main()
{
TemperatureSensor sensor = new TemperatureSensor();
sensor.TemperatureChanged = ShowTemperature;
sensor.SetTemperature(25);
sensor.SetTemperature(30);
}
static void ShowTemperature(int temperature)
{
Console.WriteLine($"現在の温度: {temperature}度");
}
}
実行結果です。
現在の温度: 25度
現在の温度: 30度
TemperatureChanged?.Invoke(_temperature) の部分で、登録されているメソッドを呼び出しています。
?. は null 条件演算子です。
通知先が登録されている場合だけ Invoke します。
マルチキャストを有効にする
デリゲートには、複数のメソッドを登録できます。
複数のメソッドを登録するには、+= を使います。
sensor.TemperatureChanged += ShowTemperature;
sensor.TemperatureChanged += WriteTemperatureLog;
sensor.TemperatureChanged += AlertIfHighTemperature;
完全な例です。
using System;
delegate void TemperatureChangedHandler(int temperature);
class TemperatureSensor
{
public TemperatureChangedHandler? TemperatureChanged;
private int _temperature;
public void SetTemperature(int temperature)
{
_temperature = temperature;
TemperatureChanged?.Invoke(_temperature);
}
}
class Program
{
static void Main()
{
TemperatureSensor sensor = new TemperatureSensor();
sensor.TemperatureChanged += ShowTemperature;
sensor.TemperatureChanged += WriteTemperatureLog;
sensor.TemperatureChanged += AlertIfHighTemperature;
sensor.SetTemperature(35);
}
static void ShowTemperature(int temperature)
{
Console.WriteLine($"現在の温度: {temperature}度");
}
static void WriteTemperatureLog(int temperature)
{
Console.WriteLine($"ログ保存: {temperature}");
}
static void AlertIfHighTemperature(int temperature)
{
if (temperature >= 30)
{
Console.WriteLine("警告: 温度が高すぎます");
}
}
}
実行結果です。
現在の温度: 35度
ログ保存: 35
警告: 温度が高すぎます
ひとつの通知に対して、画面表示、ログ保存、警告判定という複数の処理を実行できました。
このように複数の呼び出し先を持つデリゲートを、マルチキャストデリゲートと呼びます。
Invocation リスト
マルチキャストデリゲートは、内部的に Invocation リストを持っています。
Invocation リストは、呼び出し対象のメソッド一覧です。
GetInvocationList() を使うと、登録されているメソッドを確認できます。
using System;
delegate void NotifyHandler();
class Program
{
static void Main()
{
NotifyHandler handler = First;
handler += Second;
handler += Third;
foreach (Delegate item in handler.GetInvocationList())
{
Console.WriteLine(item.Method.Name);
}
}
static void First()
{
Console.WriteLine("First");
}
static void Second()
{
Console.WriteLine("Second");
}
static void Third()
{
Console.WriteLine("Third");
}
}
実行結果です。
First
Second
Third
登録した順番でメソッドが並んでいることが分かります。
通常は GetInvocationList() を日常的に使うことは多くありませんが、デリゲートが複数の呼び出し先を保持していることを理解するには便利です。
Invocation リストからターゲットを削除する
登録したメソッドは、-= で削除できます。
handler -= Second;
例を見てみます。
using System;
delegate void NotifyHandler();
class Program
{
static void Main()
{
NotifyHandler handler = First;
handler += Second;
handler += Third;
Console.WriteLine("削除前:");
handler();
handler -= Second;
Console.WriteLine("削除後:");
handler();
}
static void First()
{
Console.WriteLine("First");
}
static void Second()
{
Console.WriteLine("Second");
}
static void Third()
{
Console.WriteLine("Third");
}
}
実行結果です。
削除前:
First
Second
Third
削除後:
First
Third
Second が Invocation リストから削除されたため、2 回目の呼び出しでは実行されていません。
同じメソッドを複数回登録した場合、-= は最後に登録されたものを 1 つだけ削除します。
NotifyHandler handler = First;
handler += First;
handler += First;
handler -= First;
handler();
この場合、First は 2 回呼び出されます。
Method Group Conversion シンタックス
C# では、メソッド名だけを書いてデリゲートに代入できます。
NotifyHandler handler = Show;
この書き方を Method Group Conversion と呼びます。
本来、デリゲートオブジェクトは次のように作れます。
NotifyHandler handler = new NotifyHandler(Show);
しかし C# では、より簡潔に次のように書けます。
NotifyHandler handler = Show;
例です。
using System;
delegate void NotifyHandler(string message);
class Program
{
static void Main()
{
NotifyHandler handler1 = new NotifyHandler(Print);
NotifyHandler handler2 = Print;
handler1("handler1");
handler2("handler2");
}
static void Print(string message)
{
Console.WriteLine(message);
}
}
実行結果です。
handler1
handler2
現在の C# では、new NotifyHandler(Print) よりも Print だけを書く形が一般的です。
実践例: 注文状態の変更を通知する
もう少し実践的な例として、注文状態の変更を通知してみます。
using System;
delegate void OrderStatusChangedHandler(Order order);
class Order
{
public int Id { get; }
public string Status { get; private set; }
public OrderStatusChangedHandler? StatusChanged;
public Order(int id)
{
Id = id;
Status = "Created";
}
public void ChangeStatus(string status)
{
Status = status;
StatusChanged?.Invoke(this);
}
}
class Program
{
static void Main()
{
Order order = new Order(1001);
order.StatusChanged += SendEmail;
order.StatusChanged += WriteLog;
order.ChangeStatus("Paid");
order.ChangeStatus("Shipped");
order.StatusChanged -= SendEmail;
order.ChangeStatus("Completed");
}
static void SendEmail(Order order)
{
Console.WriteLine($"メール送信: 注文 {order.Id} は {order.Status} になりました");
}
static void WriteLog(Order order)
{
Console.WriteLine($"ログ記録: OrderId={order.Id}, Status={order.Status}");
}
}
実行結果です。
メール送信: 注文 1001 は Paid になりました
ログ記録: OrderId=1001, Status=Paid
メール送信: 注文 1001 は Shipped になりました
ログ記録: OrderId=1001, Status=Shipped
ログ記録: OrderId=1001, Status=Completed
Completed に変更する前に、次の行で SendEmail を削除しています。
order.StatusChanged -= SendEmail;
そのため、最後の状態変更ではログだけが出力されています。
フィールドとして公開する場合の注意
ここまでの例では、説明を簡単にするためにデリゲートを public フィールドとして公開しました。
public OrderStatusChangedHandler? StatusChanged;
ただし、この形だと外部から次のような操作もできてしまいます。
order.StatusChanged = null;
order.StatusChanged?.Invoke(order);
つまり、外部コードが通知先をすべて消したり、勝手に通知を発火したりできます。
実際の C# では、この問題を避けるために event キーワードを使うことが多いです。
public event OrderStatusChangedHandler? StatusChanged;
event については別の記事で詳しく扱うとして、ここでは「デリゲートはイベントの土台になる」と押さえておけば十分です。
まとめ
デリゲートを使うと、オブジェクトの状態変化を外部へ通知できます。
+=で通知先を追加できる-=で通知先を削除できる- 複数のメソッドを持つデリゲートをマルチキャストデリゲートと呼ぶ
GetInvocationList()で呼び出し対象の一覧を確認できる- メソッド名だけでデリゲートに代入する書き方を Method Group Conversion と呼ぶ
- 実際の通知 API では、デリゲートを直接公開するより
eventを使うことが多い
次回は、毎回独自の delegate 型を定義しなくても使える、Action<> と Func<> を見ていきます。