1. イベントの宣言スタイルと命名
イベントは単なるデリゲートではありません。アプリケーションの部分間のコミュニケーションの独立したエンティティであり、その宣言は分かりやすくあるべきです。
適切なデリゲート型を使う
99%の場合は標準デリゲートを使ってください:
- EventHandler — データなしのイベントに。
- EventHandler<TEventArgs> — パラメータを渡す必要があるとき。
標準化はコードの保守や .NET ライブラリとの統合を楽にします。EventHandler が使えるなら独自デリゲートは作らないでください。
public event EventHandler SomethingHappened; // データ無し
public event EventHandler<MyEventArgs> DataReceived; // 追加データあり
特別なカスタマイズが必要な場合は独自デリゲートを宣言してもいいですが、稀です。
イベントの命名
.NETではイベント名は過去形にするのが普通です:Completed, Clicked, Changed, Received。これは発生済みであることを示します。
例:
public event EventHandler DataLoaded; // データが読み込まれた
public event EventHandler<MessageEventArgs> MessageReceived; // メッセージを受信した
public event EventHandler Saving; // 保存プロセスが始まった
変更前のイベントには Changing のような形を使うこともあり、介入できる機会を与えます。
2. 発行クラスの構成:保護された仮想メソッド OnEvent
いつもイベントを呼び出す保護された仮想メソッドを追加してください:呼び出しの集中点、継承時の拡張性、挙動の予測可能性を提供します。
public class FileLoader
{
public event EventHandler<FileLoadedEventArgs> FileLoaded;
protected virtual void OnFileLoaded(FileLoadedEventArgs e)
{
FileLoaded?.Invoke(this, e);
}
public void Load(string filename)
{
// ... ファイル読み込みロジック ...
OnFileLoaded(new FileLoadedEventArgs(filename));
}
}
public class FileLoadedEventArgs : EventArgs
{
public string FileName { get; }
public FileLoadedEventArgs(string fileName) => FileName = fileName;
}
イベントを呼ぶのは OnFileLoaded のみとすることで、保守とテストが簡単になります。
3. 購読と解除のルール:ライフサイクル、IDisposable
購読者の寿命が発行者より短い場合、購読者が破棄される前に必ず解除してください。IDisposable を実装し、Dispose() 内で解除するのが便利です。
public class TemporaryListener : IDisposable
{
private readonly Publisher _publisher;
public TemporaryListener(Publisher publisher)
{
_publisher = publisher;
_publisher.DataReceived += HandleData;
}
private void HandleData(object sender, EventArgs e)
{
// データを扱う
}
public void Dispose()
{
_publisher.DataReceived -= HandleData;
}
}
// using を使った利用:
using (var listener = new TemporaryListener(myPublisher))
{
// listener はここでイベントを聞く
}
// using を抜けると Dispose が呼ばれ、解除される
解除を忘れると、発行者が購読者のデリゲートへの参照を保持してしまい、メモリリークや「ゾンビオブジェクト」が発生します。
4. イベントのスレッドセーフな呼び出し
マルチスレッドのコードでは、購読者がイベント呼び出しと並行して追加/削除されることがあります。これは競合や NullReferenceException を招きます。スレッドセーフなパターンを使って、デリゲートをローカル変数にコピーしてください。
protected virtual void OnSomethingHappened()
{
EventHandler handler = SomethingHappened;
handler?.Invoke(this, EventArgs.Empty);
}
C# 6+ では次のように十分です:
SomethingHappened?.Invoke(this, EventArgs.Empty);
5. EventArgs を使う(object を使わない)
データを object やクラスのフィールド経由で渡さないでください。EventArgs を継承した型で強い型付けを行ってください。
public class DownloadCompletedEventArgs : EventArgs
{
public string FileName { get; }
public long Size { get; }
public DownloadCompletedEventArgs(string fileName, long size)
{
FileName = fileName;
Size = size;
}
}
public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;
6. イベントと購読者のドキュメント化
いつイベントが発生するか、EventArgs のフィールドの意味、解除が必要かどうか、いつ解除すべきかをドキュメントに残してください。
/// <summary>
/// データの読み込みが成功した後に発生するイベント。
/// </summary>
public event EventHandler<DataLoadedEventArgs> DataLoaded;
7. イベント設計に関する総合的な推奨
責務を分離する
発行者は事実を通知するだけにしてください。購読者はいつ購読し、いつ解除するかを自分で決めます。
イベントの"爆撃"を避ける
同じイベントを必要もなく1秒間に何十回も発生させないでください — 無駄な負荷になります。
双方向通信にイベントを使わないようにする
イベントは「一者が通知し、多数が聞く」パターン向けです。双方向通信が必要な場合は、インターフェースやコールバック、別のメカニズムを検討してください。
クラス内に購読者を保持しない
購読者への明示的な参照を保持しないでください — イベントとデリゲートが自動的にそれを扱います。
8. 古典的なアンチパターン
型のないイベント
public event Action<object> SomethingHappened; // 中身が不明瞭
悪い例:型安全が壊れ、キャストが必要になり、保守性が落ちます。
解除を忘れる
public class ShortLivedListener
{
public ShortLivedListener(Publisher p) =>
p.DataReceived += DoWork;
private void DoWork(object sender, EventArgs e) { /* ... */ }
// Dispose が無く、解除も無し => ゾンビオブジェクト!
}
SRP の違反
クラスが同時に発行者であり購読者でありハンドラである — 役割が混在しています。責務を分けてください。
9. 面接やプロジェクトでの実践的適用
多くのプロジェクトで publish-subscribe の整理がしっかりしていることがスケーラビリティと保守性の鍵になります。面接ではしばしば次のようなことを求められます:
- 正しい型付けでイベントシステムを実装する、
- 購読者のライフサイクル管理を示す、
- イベントのスレッドセーフな呼び出しを説明する。
きれいでドキュメント化され、正しく構成されたイベントコードは候補者の中であなたを際立たせます。
GO TO FULL VERSION