第67章:Observer ②:C#標準(event / EventHandler)🔔
ねらい 🎯

- 「Observer(観測者)」を C#の“標準のやり方”=
eventでスッと説明できるようになる📣 - 発行側(Subject) が 購読者(Observer)を知らなくてよい 状態を、実コードで体感する🧩✨
.NETの定番APIを読んで「どこが差し替え点?」を見抜く目を作る👀💡
到達目標 ✅
event EventHandler<TEventArgs>の意味を説明できる(senderとe)🔎- 購読(
+=)と解除(-=)を正しく使い、解除漏れの怖さも説明できる😱🧯 - 「Observerを入れると、呼び出し側のif/switchが減る理由」を言語化できる🧠✨
- 「この場面はイベントでOK / これは別の仕組みが必要」を判断できる⚖️
手順 🧭
1) ObserverをC#に翻訳するとこうなる 📘✨
Observerはざっくり言うと、
- 変化を起こす人(発行側)がいて 👑
- それを見て反応する人(購読者)がいて 👀
- 発行側は「誰が見てるか」を知らなくてもOK 🤝
C#だとこの関係を、event と EventHandler で自然に表現できるよ〜🔔
event は「外から勝手に呼ばれないように、購読だけ許す」ためのキーワードだよ📌 (Microsoft Learn)
2) “.NET標準のイベント形”を覚える(これが本命)🌟
Microsoftのガイドでも、通常は EventHandler / EventHandler<TEventArgs> を使えば良い(独自delegateは基本いらない)って方針だよ🧡 (Microsoft Learn)
EventHandler<TEventArgs> 自体も「イベント用の定義済みデリゲート」だよ〜って書かれてる📚 (Microsoft Learn)
まずは形を丸ごと覚えちゃうのが早い✨
public event EventHandler? SomethingHappened; // 情報なし(EventArgs.Empty)
public event EventHandler<SomethingEventArgs>? SomethingHappened2; // 情報あり(EventArgs派生)
ポイントはこれ👇
sender:誰が発行した?(たいていthis)e:イベントの情報(EventArgsか、その派生)
3) “本物のObserver”を標準APIで見る 👀🔍
「イベント=Observer」を体感するなら、このへんが超定番✨
FileSystemWatcher:ファイル変更をイベントで通知する📂🔔 (Microsoft Learn)System.Timers.Timer:一定間隔でイベント発火する⏰💥(※スレッド注意あり) (Microsoft Learn)INotifyPropertyChanged.PropertyChanged:UIバインディング等で「値変わったよ」を通知する🪞✨ (Microsoft Learn)
ここで大事なのは「APIの使い手として、差し替え点はどこ?」って見ること👀 → 購読者(ハンドラ)を追加/差し替えできるのが差し替え点だよ💡
4) ミニ模倣コードを書く(イベント発行側=Subject)🛒🔔
「注文確定したら通知が飛ぶ」ミニモデルでやってみよう✨ (学習用に最小!“汎用フレームワーク”とか作らないよ🙂)
using System;
public sealed class OrderConfirmedEventArgs : EventArgs
{
public OrderConfirmedEventArgs(int orderId, decimal total)
{
OrderId = orderId;
Total = total;
}
public int OrderId { get; }
public decimal Total { get; }
}
public sealed class OrderService
{
// Observer(購読者)に向けた“通知口”
public event EventHandler<OrderConfirmedEventArgs>? OrderConfirmed;
public void ConfirmOrder(int orderId, decimal total)
{
// ここに本来は在庫引当とか支払い確定とかある想定🙂
OnOrderConfirmed(new OrderConfirmedEventArgs(orderId, total));
}
private void OnOrderConfirmed(OrderConfirmedEventArgs e)
{
// よくある安全な呼び方:ローカルに退避してからInvoke
var handler = OrderConfirmed;
handler?.Invoke(this, e);
}
}
ここでの美味しさ🍰
OrderServiceは 通知先(メール/SMS/ログ)を知らない- 追加したい時は購読者を増やすだけでOK(疎結合)✨
5) 購読者(Observer)を“関数1個”で追加する 📩📣
C#のイベント購読はほんとにシンプル:
public static class Observers
{
public static void SendEmail(object? sender, OrderConfirmedEventArgs e)
{
Console.WriteLine($"📧 Email: Order={e.OrderId}, Total={e.Total}");
}
public static void SendSms(object? sender, OrderConfirmedEventArgs e)
{
Console.WriteLine($"📱 SMS: Order={e.OrderId}, Total={e.Total}");
}
}
// 使い方
// var svc = new OrderService();
// svc.OrderConfirmed += Observers.SendEmail;
// svc.OrderConfirmed += Observers.SendSms;
// svc.ConfirmOrder(123, 9800m);
解除も同じくらい大事⚠️
// 解除(これを忘れるとメモリリークになりやすい😱)
svc.OrderConfirmed -= Observers.SendEmail;
6) テストで「購読したら呼ばれる / 解除したら呼ばれない」を固定する 🧪✨
Observer系は テストで“契約”を固めると安心感が爆上がりするよ〜🌸
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class OrderServiceTests
{
[TestMethod]
public void OrderConfirmed_WhenSubscribed_CallsHandler()
{
var svc = new OrderService();
var called = false;
svc.OrderConfirmed += (_, e) =>
{
called = true;
Assert.AreEqual(1, e.OrderId);
Assert.AreEqual(100m, e.Total);
};
svc.ConfirmOrder(1, 100m);
Assert.IsTrue(called);
}
[TestMethod]
public void OrderConfirmed_WhenUnsubscribed_DoesNotCallHandler()
{
var svc = new OrderService();
var called = 0;
void Handler(object? _, OrderConfirmedEventArgs __) => called++;
svc.OrderConfirmed += Handler;
svc.OrderConfirmed -= Handler;
svc.ConfirmOrder(1, 100m);
Assert.AreEqual(0, called);
}
}
7) AI補助を使うなら「出力の型」を固定してお願いする 🤖📝
イベント周りは“それっぽい独自抽象”を作りがちなので、AIに頼むなら縛るのがコツ💡
C#で .NET の標準イベントパターン(event + EventHandler<TEventArgs>)だけを使ってください。
独自delegateやイベントバス/汎用フレームワークは作らないでください。
発行側は購読者を知らない設計にしてください。
購読解除(-=)ができる例も入れて、MSTestで2本テストを書いてください。
落とし穴 ⚠️😵
-
解除漏れ(メモリリーク) 🧟♀️ 発行側が長生きで、購読者が短命だと、購読者が回収されにくくなることがあるよ💦 →
-=を忘れない / 必要ならDisposeでまとめて解除する設計にする🧹 -
イベントの中で例外が出ると、後続の購読者が呼ばれない 💥 1人が例外を投げると、そこで止まっちゃう挙動になりがち😱 → 「例外は止めるべき?続けるべき?」を決める(続けるなら1購読者ずつtry/catch)🧯
-
スレッドが違う場所で呼ばれることがある 🧵⚡ たとえば
Timer.Elapsedは条件によって ThreadPool で呼ばれうるよ、って注意がある(=再入可能性も考える)⏰🌀 (Microsoft Learn) → UI更新するならUIスレッドへ戻す(仕組みが必要)🪟 -
“イベントが1回だけ”だと思い込む 🔁
FileSystemWatcherみたいに、環境でイベントが複数回/想定外に起きる注意があるよ📂⚠️ (Microsoft Learn) → 購読側で「重複を許す設計」や「抑制(デバウンス)」を考える🧠 -
Actionイベントに寄せすぎる 🤏 もちろん書けるけど、標準の文脈(sender/EventArgs)から外れると読み手が迷いやすい💦 → “.NETの流儀”に合わせるならEventHandler<TEventArgs>が安心✨ (Microsoft Learn)
演習 🧪🍰
演習1:購読者を3つに増やす(通知が増えても発行側は無改造)📣📣📣
-
OrderServiceのOrderConfirmedを使って、購読者を3つ作るEmail(ConsoleでOK)📧Sms(ConsoleでOK)📱AuditLog(ConsoleでOK)📝
-
ConfirmOrderを1回呼んで、3つ全部が動くのを確認する🎉
演習2:解除漏れ対策を入れる(Disposeでまとめて解除)🧹✨
- 購読者側を
IDisposableにして、Disposeで-=するようにする - テストで「Dispose後は呼ばれない」を確認する🧪✅
演習3:INotifyPropertyChanged っぽく触ってみる(観測の王道)🪞💎
ProductにPriceプロパティを作る- 変更時に
PropertyChangedを発火(PropertyChangedEventArgsを使う) PropertyNameにnullや空文字で「全部変わった」扱いにできる、みたいな“現実のルール”を軽く読む📚 (Microsoft Learn)
チェック ✅📌
eventが「購読はOK、外部からの発火はNG」を守る理由を言える?🔒 (Microsoft Learn)EventHandler<TEventArgs>のsenderとeの役割を説明できる?👀- 購読(
+=)と解除(-=)を、テストで保証できてる?🧪 - 発行側が「通知先の種類」を知らない状態になってる?(疎結合できてる?)🧩✨
- 例外・スレッド・解除漏れの3点を、最低1つは対策できてる?⚠️🧯