前回までは、デリゲートについて見てきました。
今回は、デリゲートを土台にした C# のイベントを整理します。
イベントは、「あるオブジェクトで何かが起きたこと」を外部に通知するための仕組みです。
たとえば、次のような場面で使われます。
- ボタンがクリックされた
- ファイルの読み込みが完了した
- 注文の状態が変更された
- センサーの値がしきい値を超えた
デリゲートだけでも通知はできますが、event キーワードを使うことで、外部からできる操作を制限し、より安全な通知 API として公開できます。
event キーワードとは
event キーワードは、デリゲートをイベントとして公開するためのキーワードです。
まずは、デリゲートを直接公開する例を見てみます。
using System;
delegate void OrderStatusChangedHandler(Order order);
class Order
{
public OrderStatusChangedHandler? StatusChanged;
public string Status { get; private set; } = "Created";
public void ChangeStatus(string status)
{
Status = status;
StatusChanged?.Invoke(this);
}
}
このコードでも、状態変更の通知はできます。
しかし、StatusChanged は public フィールドなので、外部から次のような操作ができてしまいます。
order.StatusChanged = null;
order.StatusChanged?.Invoke(order);
外部コードが通知先をすべて消したり、勝手にイベント相当の処理を呼び出したりできるということです。
そこで event を使います。
using System;
delegate void OrderStatusChangedHandler(Order order);
class Order
{
public event OrderStatusChangedHandler? StatusChanged;
public string Status { get; private set; } = "Created";
public void ChangeStatus(string status)
{
Status = status;
StatusChanged?.Invoke(this);
}
}
event を付けると、外部からは基本的に += と -= だけができます。
order.StatusChanged += OnStatusChanged;
order.StatusChanged -= OnStatusChanged;
一方、次のような操作は外部からできません。
order.StatusChanged = null; // できない
order.StatusChanged?.Invoke(order); // できない
イベントを発火できるのは、イベントを宣言しているクラスの内側だけです。
これが、event キーワードの重要な役割です。
イベントの中身はどうなっているのか
フィールドのように見えるイベントは、内部的には add と remove というアクセサーを持つメンバーとして扱われます。
たとえば、次のイベントを考えます。
public event EventHandler? Completed;
外部から次のように書くと、
worker.Completed += OnCompleted;
worker.Completed -= OnCompleted;
概念的には、次のような add / remove 処理が呼ばれます。
private EventHandler? _completed;
public event EventHandler? Completed
{
add
{
_completed += value;
}
remove
{
_completed -= value;
}
}
このように、イベントは「外部から購読と購読解除だけを許可するための入り口」と考えると分かりやすいです。
イベントを発火するときは、クラスの内部で保持しているデリゲートを呼び出します。
_completed?.Invoke(this, EventArgs.Empty);
通常は、次のようなフィールド風イベントを使えば十分です。
public event EventHandler? Completed;
コンパイラが裏側で必要な仕組みを用意してくれます。
イベントを購読する
イベントを受け取る側は、+= でイベントハンドラーを登録します。
次の例では、処理が完了したときに Completed イベントを発生させます。
using System;
class ReportGenerator
{
public event EventHandler? Completed;
public void Generate()
{
Console.WriteLine("レポートを作成しています...");
// 実際にはここで時間のかかる処理を行う
Completed?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
ReportGenerator generator = new ReportGenerator();
generator.Completed += OnReportCompleted;
generator.Generate();
}
static void OnReportCompleted(object? sender, EventArgs e)
{
Console.WriteLine("レポート作成が完了しました");
}
}
実行結果です。
レポートを作成しています...
レポート作成が完了しました
Completed?.Invoke(this, EventArgs.Empty) の第 1 引数には、イベントを発生させたオブジェクトを渡します。
第 2 引数には、イベントに関する追加情報を渡します。追加情報がない場合は、EventArgs.Empty を使います。
イベントを購読解除する
イベントハンドラーは、-= で解除できます。
generator.Completed -= OnReportCompleted;
複数回通知を受けたくない場合や、オブジェクトの寿命をまたいで参照が残ることを避けたい場合に重要です。
using System;
class TimerLikeObject
{
public event EventHandler? Tick;
public void RaiseTick()
{
Tick?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
TimerLikeObject timer = new TimerLikeObject();
timer.Tick += OnTick;
timer.RaiseTick();
timer.Tick -= OnTick;
timer.RaiseTick();
}
static void OnTick(object? sender, EventArgs e)
{
Console.WriteLine("Tick を受け取りました");
}
}
実行結果です。
Tick を受け取りました
2 回目の RaiseTick() の前に購読解除しているため、メッセージは 1 回だけ表示されます。
Visual Studio でイベント登録を簡単にする
Visual Studio では、イベントハンドラーの登録を補助してくれます。
コード上でイベントに += を入力すると、IntelliSense が候補を表示します。
button.Click +=
ここで Tab キーを使うと、イベントハンドラーのメソッドを自動生成できます。
たとえば、Windows Forms や WPF のボタンでは、次のようなメソッドが作られます。
private void button_Click(object sender, EventArgs e)
{
}
また、Windows Forms のデザイナーでは、プロパティウィンドウのイベント一覧からイベントハンドラーを作成できます。
WPF の XAML でも、次のようにイベント名を書けます。
<Button Content="保存"
Click="SaveButton_Click" />
対応するコードビハインドには、次のようなメソッドを用意します。
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
// 保存処理
}
Visual Studio の補完を使うと、イベントのシグネチャを手で覚えていなくても、正しい形のハンドラーを作りやすくなります。
カスタムイベント引数を作成する
イベントに追加情報を渡したい場合は、EventArgs を継承したクラスを作ります。
たとえば、注文状態が変わったときに、古い状態と新しい状態を通知したいとします。
using System;
class OrderStatusChangedEventArgs : EventArgs
{
public string OldStatus { get; }
public string NewStatus { get; }
public OrderStatusChangedEventArgs(string oldStatus, string newStatus)
{
OldStatus = oldStatus;
NewStatus = newStatus;
}
}
このイベント引数を使って、注文クラスを書きます。
using System;
class Order
{
public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;
public string Status { get; private set; } = "Created";
public void ChangeStatus(string status)
{
string oldStatus = Status;
Status = status;
StatusChanged?.Invoke(
this,
new OrderStatusChangedEventArgs(oldStatus, Status)
);
}
}
購読する側は、イベント引数から詳細情報を取得できます。
using System;
class Program
{
static void Main()
{
Order order = new Order();
order.StatusChanged += OnStatusChanged;
order.ChangeStatus("Paid");
order.ChangeStatus("Shipped");
}
static void OnStatusChanged(object? sender, OrderStatusChangedEventArgs e)
{
Console.WriteLine($"{e.OldStatus} から {e.NewStatus} に変わりました");
}
}
実行結果です。
Created から Paid に変わりました
Paid から Shipped に変わりました
イベントに意味のある情報を渡したいときは、専用の EventArgs クラスを作ると読みやすくなります。
ジェネリック EventHandler デリゲート
.NET には、イベント用の標準デリゲートとして EventHandler と EventHandler<TEventArgs> が用意されています。
追加情報がない場合は、EventHandler を使います。
public event EventHandler? Completed;
追加情報がある場合は、EventHandler<TEventArgs> を使います。
public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;
EventHandler<TEventArgs> は、概念的には次のようなデリゲートです。
delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
第 1 引数の sender は、イベントを発生させたオブジェクトです。
第 2 引数の e は、イベントに関する追加情報です。
この形は .NET のイベントで広く使われています。
型引数に技術的な制約はありませんが、慣習として EventArgs またはその派生クラスを渡すのが一般的です。
そのため、独自のイベントでも、特別な理由がなければ EventHandler または EventHandler<TEventArgs> を使うとよいでしょう。
実践例: ファイル監視のような通知
最後に、少し実践的な例として、ファイルが追加されたことを通知するクラスを作ってみます。
using System;
using System.Collections.Generic;
class FileAddedEventArgs : EventArgs
{
public string FileName { get; }
public FileAddedEventArgs(string fileName)
{
FileName = fileName;
}
}
class FileList
{
private readonly List<string> _files = new List<string>();
public event EventHandler<FileAddedEventArgs>? FileAdded;
public void Add(string fileName)
{
_files.Add(fileName);
FileAdded?.Invoke(this, new FileAddedEventArgs(fileName));
}
}
class Program
{
static void Main()
{
FileList files = new FileList();
files.FileAdded += OnFileAdded;
files.Add("report.pdf");
files.Add("summary.xlsx");
}
static void OnFileAdded(object? sender, FileAddedEventArgs e)
{
Console.WriteLine($"ファイルが追加されました: {e.FileName}");
}
}
実行結果です。
ファイルが追加されました: report.pdf
ファイルが追加されました: summary.xlsx
FileList は、ファイル追加後に何をするかを知りません。
通知を受け取った側が、ログ出力、画面更新、メール送信などの処理を自由に追加できます。
まとめ
イベントは、オブジェクトで発生した出来事を外部へ通知するための仕組みです。
eventキーワードは、デリゲートを安全な通知 API として公開する- 外部からは基本的に
+=と-=だけができる - イベントを発火できるのは、イベントを宣言したクラスの内部だけ
- イベントは内部的に
add/removeアクセサーを持つ - 追加情報がないイベントには
EventHandlerを使う - 追加情報があるイベントには
EventHandler<TEventArgs>を使う - 独自のイベント情報は
EventArgsを継承したクラスで表す
デリゲートは「呼び出せる処理を表す型」でした。
イベントは、そのデリゲートを使って「外部に通知するための公開口」を作る仕組みです。
次回は、イベント登録でもよく使われる匿名メソッドを見ていきます。