CodeGym /コース /C# SELF /実践シナリオとベストプラクティス

実践シナリオとベストプラクティス

C# SELF
レベル 53, レッスン 4
使用可能

1. イベントの実践的な使用シナリオ

イベントは教科書の美しい理論だけじゃありません。実際にはほとんどのアプリで必要で、UIやネットワークサービスを扱うならほとんど常に使われます。

いくつか典型的な実務シナリオを見てみましょう。同時に標準やベストプラクティスがよくあるミスをどう防ぐかも確認します。

ユーザー操作に反応する(UIのように)

おそらく最も頻繁なシナリオは、ユーザーが何かをしたとき(ボタンを押す、リストの項目を選ぶ)にアプリが反応するケースです。Windows Forms、WPF、UWP、Avalonia、MAUIなどのUIフレームワークはまさにこれで動いてます。

ミニ例(UIなしでボタンを模擬):


public class Button
{
    public event EventHandler? Click; // 標準のデリゲート

    public void SimulateClick()
    {
        Click?.Invoke(this, EventArgs.Empty); // 「ボタン」が「クリックされた」
    }
}

class Program
{
    static void Main()
    {
        var button = new Button();
        button.Click += (sender, e) => Console.WriteLine("ボタンが押されました!");
        button.SimulateClick(); // > ボタンが押されました!
    }
}

実際のUIフレームワークでは、Clickイベントはユーザーがマウスでボタンを押したり画面をタップしたりしたときに発生します。イベントに登録したものは全て通知を受け取り、アプリのロジックはそうやって動きます。

進捗、完了、エラーの通知

ファイルのダウンロード、データ処理、長時間の計算などを想像してみてください。進捗やエラーを通知する必要があります。よく「進捗のステップ」用のイベントと「完了」や「失敗」用の別イベントを用意します。

ファイルダウンローダの例:


public class FileDownloader
{
    public event EventHandler<int>? ProgressChanged;
    public event EventHandler? DownloadCompleted;
    public event EventHandler<string>? DownloadFailed;

    public void Download()
    {
        for (int i = 1; i <= 100; i += 10)
        {
            Thread.Sleep(50); // 遅延の模擬
            ProgressChanged?.Invoke(this, i);
        }
        DownloadCompleted?.Invoke(this, EventArgs.Empty);
    }
}

var downloader = new FileDownloader();
downloader.ProgressChanged += (s, progress) => Console.WriteLine($"ダウンロード: {progress}%");
downloader.DownloadCompleted += (s, e) => Console.WriteLine("ダウンロードが完了しました。");
downloader.Download();

ここでは複数のイベントがあります:全部に登録してもいいし、必要なものだけ登録してもいい。これは多くの標準的な.NETクラス(スレッド、ファイルダウンロード、HTTP操作など)が動作するのと似ています。

オブジェクトのライフサイクルへの反応(例:「保存された」「削除された」)

ドメインモデルがあるとします。ユーザーがオブジェクトを保存・削除したときに、キャッシュ更新やログ、メッセージ送信など別の処理が反応してほしい、というケースです。


public class UserRepository
{
    public event EventHandler<UserEventArgs>? UserSaved;
    public event EventHandler<UserEventArgs>? UserDeleted;

    public void Save(User user)
    {
        //... ユーザーを保存
        UserSaved?.Invoke(this, new UserEventArgs(user));
    }

    public void Delete(User user)
    {
        //... ユーザーを削除
        UserDeleted?.Invoke(this, new UserEventArgs(user));
    }
}

public class UserEventArgs : EventArgs
{
    public User User { get; }
    public UserEventArgs(User user) => User = user;
}

これで別モジュールが直接の依存なしに購読でき、ユーザーの変更について通知を受け取れます。リポジトリ自身は誰が接続しているかを知る必要がありません。

非同期イベントとマルチスレッド

イベントは時々メイン(UI)スレッド以外から発生します — 例えばバックグラウンドタスク、タイマー、非同期操作などから。そういう場合、ハンドラのコードは「別スレッド」で実行されることに注意が必要です。ハンドラが別スレッドから直接UI要素を更新しようとすると例外になります。

マーシャリングとは何か? マーシャリングはコードの実行をあるスレッドから別のスレッドに移すこと、通常はバックグラウンドスレッドからUIスレッドに戻して安全にUIを更新するために使います。UIアプリ(WinForms、WPF)では、SynchronizationContextやDispatcherのような仕組みを使って、ハンドラの呼び出しを望むスレッドに「リダイレクト」します。

やってはいけない例:


// イベントがバックグラウンドスレッドから呼ばれ、ハンドラがUIを直接更新すると例外になる!

推奨: 実行スレッドを確認し、必要ならUIスレッドへのマーシャリングを行ってください(SynchronizationContextやDispatcher経由)。コンソールやサーバーアプリでは通常こうした制約はありません。

Event Aggregator / Messaging

大規模なアプリでは中心的なイベント集約器(Event Aggregator)を使うことが多いです。これにより結合度が下がります:サブスクライバとパブリッシャは互いを知らず、中心を介してメッセージを交換します。


public class EventAggregator
{
    public event EventHandler<SomeEventArgs>? SomeEvent;

    public void Publish(SomeEventArgs args)
    {
        SomeEvent?.Invoke(this, args);
    }
}

2. ベストパターンとプラクティス

エラーのために[CallerMemberName]属性を使う

インフラ用コード(ロガーやトレーサーなど)を作るなら、CallerMemberName属性を使ってイベントの発生元メソッド名をログに残すと診断が楽になります。

必要ならイベントをスレッドセーフに保つ

イベントはマルチキャストで、異なるスレッドからの操作は注意が必要です:呼び出す前にデリゲートをローカル変数にコピーしてください。


var handler = MyEvent;
if (handler != null) handler(this, args);

C# 6+では安全な呼び出しを使えます:MyEvent?.Invoke(this, args)。

カスタムのEventArgsはimmutableにする

カスタムのEventArgs型は読み取り専用プロパティで定義しましょう — これによりサブスクライバがイベントの状態を誤って変更することを防げます。


public class WorkCompletedEventArgs : EventArgs
{
    public string Message { get; }
    public WorkCompletedEventArgs(string message) => Message = message;
}

publicかprivateのevent

イベントはpublicなeventにして、発火はprotectedなvirtualメソッドOnEventNameでカプセル化してください。これにより拡張性が確保され(派生クラスが動作をオーバーライドできる)、外部は直接イベントを発火できなくなります。

ライフサイクルの順序を壊さない

長いハンドラチェーンを起動する場合、誰かが購読して内部ロジックを変えようとするかもしれないことを忘れないでください。イベントがどこでいつ発火するか、何回発火する可能性があるかをドキュメント化しましょう。

複数イベントと構成

あるオブジェクトが複数のソースを監視する場合、購読と購読解除を一箇所にまとめてください(例えば、Subscribe/Unsubscribeメソッド)。必要なら、購読解除を簡単にするためにプライベートなデリゲートフィールドを保持します。

3. イベントのやってはいけない使い方:典型的なミス

ミス #1: 標準的なイベントパターンを無視する。
もし各クラスがそれぞれ独自のデリゲートをイベントごとに定義すると、すぐにカオスになります。標準のEventHandlerやEventHandler<T>(TはEventArgsを継承)を使うようにしましょう。こうすると.NET開発者にとってコードが読みやすく、馴染みやすくなります。

ミス #2: 外部コードからイベントを不適切に呼び出す。
絶対にイベントをパブリッシャークラスの外で直接呼び出してはいけません。イベントは通常クラス内部だけで、保護されたメソッドOnEventNameを通して発生させます。サブスクライバは購読(+=)と解除(-=)だけの権利を持ちます。

悪い例:


foo.MyEvent(); // エラー!外部からイベントを直接呼び出してはいけない

良い例:


// fooの内部でのみ:
// protected virtual void OnMyEvent()
// {
//     MyEvent?.Invoke(this, EventArgs.Empty);
// }

ミス #3: 購読者の有無(null)を確認せずにイベントを呼ぶ。
購読者がいないイベントを呼ぶと、NullReferenceExceptionを受け取ることになります。C# 6.0+では?.Invoke(...)を使って、購読者がいるときだけ呼び出すようにしてください。

ミス #4: イベントの購読解除を忘れる(メモリリーク)。
あるオブジェクトがイベントに購読していて、破棄前に解除しないと、パブリッシャーがサブスクライバへの参照を保持し続けます。ガベージコレクタはそのメモリを解放できず、長寿命のパブリッシャーと短命のサブスクライバ(例えばViewModelやフォーム)の組み合わせでは重大です。最善の解決策は明示的に解除するか、適切な場合は弱参照(WeakReference)を使うことです。

ミス #5: protected virtualメソッドOnEventを使わずにイベントを直接呼び出す。
イベントを直接呼ぶと、派生クラスが正しく挙動をオーバーライドする機会を奪います。正しいパターンは、イベントを発火する保護された仮想メソッドを宣言することです。

標準的な例:


protected virtual void OnSomething(EventArgs e)
{
    Something?.Invoke(this, e);
}
2
タスク
C# SELF, レベル 53, レッスン 4
ロック未解除
オブジェクトのライフサイクルをイベントで管理しよう
オブジェクトのライフサイクルをイベントで管理しよう
1
アンケート/クイズ
イベントのライフサイクル、レベル 53、レッスン 4
使用不可
イベントのライフサイクル
イベント購読の詳細な解説
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION