C# で「ある変数の値が変わった瞬間に処理を走らせたい」というニーズは、業務アプリやツール開発で非常によく登場します。本記事では、プロパティの変更をトリガーに処理を実行する王道パターンから、軽量な代替案、UI バインディング向けの INotifyPropertyChanged まで、実務レベルで使える具体例と注意点をまとめて解説します。
C#で「変数の変更をトリガーに処理を走らせる」とは?
C# では、単純なフィールド(変数)が変更されたことを、ランタイムが自動で検知してくれるわけではありません。int age; のようなフィールドの値を変更しても、「変わったよ」という通知は自動では飛びません。
そこでよく使われるのがプロパティの setterを入り口とした実装です。値を直接フィールドではなくプロパティ経由で設定し、その中で「値が変わったかどうか」を判定して、変わったタイミングだけでイベントを発火します。
この記事では、次の3パターンを軸に解説していきます。
- 標準的なイベントパターン:
EventHandler<TEventArgs>を使う方法 - 軽量な代替案:
Actionなどのデリゲートを公開してしまう方法 - UI バインディング向け:
INotifyPropertyChangedインターフェースを実装する方法
まずは、.NET の王道ともいえる EventHandler パターン から見ていきましょう。
王道パターン:プロパティの setter + EventHandler<TEventArgs>
最もベーシックで汎用的なのが、専用のイベントとイベント引数クラスを定義して、プロパティの setter でイベントを発火するパターンです。以下は、Age プロパティの変更を検知して通知するクラスの例です。
using System;
public class Person
{
private int _age;
// 1) イベント引数クラス
public sealed class AgeChangedEventArgs : EventArgs
{
public int Age { get; }
public AgeChangedEventArgs(int age)
{
Age = age;
}
}
// 2) イベント本体(標準の EventHandler<T> を利用)
public event EventHandler<AgeChangedEventArgs>? AgeChanged;
// 3) 発火用の保護メソッド(継承で差し替え可能にするのが慣例)
protected virtual void OnAgeChanged(int newAge)
{
AgeChanged?.Invoke(this, new AgeChangedEventArgs(newAge));
}
// 4) 値が変わったときだけ発火するプロパティ
public int Age
{
get => _age;
set
{
if (_age == value)
{
// 同じ値なら何もしない(無駄な通知を防ぐ)
return;
}
_age = value;
OnAgeChanged(_age);
}
}
// (任意)同一クラス内で自分自身に購読する例
public Person()
{
AgeChanged += (_, e) => LogAge(e.Age);
}
private void LogAge(int age)
{
Console.WriteLine($"年齢が {age} に変わりました");
}
}
この実装を外部から利用するときは、次のようにイベントに購読(+=)し、プロパティを変更します。
var p = new Person();
// 外部からの購読
p.AgeChanged += (sender, e) =>
{
Console.WriteLine($"通知を受け取りました。新しい年齢: {e.Age}");
};
p.Age = 10; // このタイミングで AgeChanged イベントが発火する
p.Age = 10; // 値が変わらないのでイベントは発火しない
p.Age = 20; // 再度イベントが発火する
このパターンを構成する要素を整理すると、以下のようになります。
| 要素 | 役割 | 実装例 |
|---|---|---|
| イベント引数 | 変更された値など、通知したい情報をまとめるクラス | AgeChangedEventArgs |
| イベント本体 | 購読者が登録されるイベント | public event EventHandler<AgeChangedEventArgs>? AgeChanged; |
| 発火メソッド | イベントを呼び出すための共通メソッド | protected virtual void OnAgeChanged(int newAge) |
| プロパティ setter | 値が変わったかチェックし、変わったときだけ発火 | Age プロパティの set |
| 購読者 | イベントに += して処理を登録する側 | p.AgeChanged += ... |
この構成をひとつテンプレートとして覚えておくと、どのプロパティに対しても同じ考え方で「変更をトリガーに処理を実行する」仕組みを実装できます。
イベントとデリゲートの基礎をざっくり整理
「デリゲートとイベントの違いがよく分からない」という声も多いので、ここで簡単に整理しておきます。
- デリゲート:メソッドを「変数」として扱うための型(関数ポインタのようなもの)
- イベント:デリゲートを外部公開するときの「安全なラッパー」
デリゲート単体でもコールバックの登録はできますが、event キーワードを付けることで、外部からは += と -= だけが許可され、勝手に呼び出したり上書きしたりできなくなります(カプセル化が向上します)。
| 項目 | event | デリゲート フィールド(例:Action) |
|---|---|---|
| 外部からの呼び出し | 不可(クラス内部からのみ呼べる) | 可能(フィールドにアクセスできれば呼べてしまう) |
| 外部からの再代入 | 不可(+= / -= のみ) | 可能(= で上書きされる危険がある) |
| 用途 | 通知・購読モデル(イベント) | コールバックの受け渡しなど、より自由な用途 |
| 推奨度 | 外部に通知したいときは基本こちら | 内部専用、または割り切った API に限定 |
「外部に対して何かが起きたことを通知したい」「購読者が複数いるかもしれない」といったケースでは、基本的に event を使う と覚えておくと安全です。
同一クラス内・外部クラスからのイベント購読パターン
先ほどの Person クラスでは、コンストラクター内で自分自身のイベントに購読していました。
public Person()
{
AgeChanged += (_, e) => LogAge(e.Age);
}
このように「同じクラスの中で完結する処理」であれば、コンストラクターで自分自身に登録してしまうのがシンプルです。「このクラス内部では、年齢が変わるたびにログを書きたい」といった用途に向きます。
一方で、「他のクラスに通知して、別の処理を行いたい」場合は、外部で次のように購読します。
public class PersonObserver
{
public void Run()
{
var person = new Person();
person.AgeChanged += OnPersonAgeChanged;
person.Age = 30;
person.Age = 31;
}
private void OnPersonAgeChanged(object? sender, Person.AgeChangedEventArgs e)
{
Console.WriteLine($"Observer: 年齢が {e.Age} に変更されました。");
}
}
現場でありがちなパターンとしては、次のようなものがあります。
- ドメインモデル(
Personなど)の変更を、UI やログ、監視クラスに通知する - 設定値オブジェクトの変更を、実行中のサービスに伝える(例:ログレベルの動的変更)
- バックグラウンド処理の進捗(
Progress)を UI に通知する
このとき、「イベントをどこまで公開するか」「どの層から購読させるか」を意識して設計すると、後から仕様変更が入っても修正しやすくなります。
軽量な代替案:Action を使った公開デリゲート
「イベントというほど大げさな仕組みはいらない」「購読者は 1 箇所だけでよい」といったケースでは、シンプルに Action を公開するだけ という手もあります。
public class PersonLite
{
private int _age;
// イベントではなく、公開デリゲート
public Action<int>? OnAgeChanged;
public int Age
{
get => _age;
set
{
if (_age == value)
{
return;
}
_age = value;
OnAgeChanged?.Invoke(_age);
}
}
}
使い方も非常にシンプルです。
var p = new PersonLite();
// コールバックを 1 つ登録するだけ
p.OnAgeChanged = age => Console.WriteLine($"Lite: {age}");
p.Age = 5;
p.Age = 6;
ただし、この方式には注意点があります。
- 外部から
p.OnAgeChanged = null;と上書きできてしまう - 複数の購読者を簡単に扱う設計にはなっていない(自分で
+=/-=を実装する必要がある) - 「通知モデル」というより「コールバックを 1 個渡してもらう」イメージに近い
そのため、ライブラリや共通クラスとして広く使われる API には向きません。しかし、アプリ内部で完結する一時的なクラスや、小規模ツールの中だけで使うのであれば、コード量を減らせるので十分に有効な選択肢です。
複数プロパティの変更を扱うなら INotifyPropertyChanged
WPF・WinForms・MAUI などの UI フレームワークでは、プロパティが変わったことをビュー(画面)に通知し、自動的に UI を更新させたい場面が多くあります。このとき定番となるのが、INotifyPropertyChanged インターフェースです。
INotifyPropertyChanged は非常にシンプルで、次の 1 つのイベントだけを持つインターフェースです。
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler? PropertyChanged;
}
このイベントに対して、プロパティ名を文字列で通知していく、というスタイルになります。実装例を見てみましょう。
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class PersonViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
// 呼び出し元のプロパティ名を自動補完するユーティリティメソッド
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
if (propertyName is null)
{
return;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private int _age;
public int Age
{
get => _age;
set
{
if (_age == value)
{
return;
}
_age = value;
OnPropertyChanged(); // propertyName に "Age" が自動で入る
}
}
private string? _name;
public string? Name
{
get => _name;
set
{
if (_name == value)
{
return;
}
_name = value;
OnPropertyChanged(); // こちらは "Name" が自動で通知される
}
}
}
このような ViewModel を WPF の XAML などからバインディングすると、Age や Name をコード側で変更したときに、自動的に UI が更新されます。
「専用イベントを作る場合」と「INotifyPropertyChanged を使う場合」の違いをざっくり比較すると、次のようになります。
| 観点 | 専用イベント(例:AgeChanged) | INotifyPropertyChanged |
|---|---|---|
| 通知の粒度 | プロパティごとにイベントを分ける | プロパティ名を文字列で通知(1本のイベント) |
| 主な用途 | ドメインイベント、ロジック層での通知 | UI バインディング、MVVM |
| 拡張性 | イベントごとに専用の引数を設計できる | 基本は「どのプロパティが変わったか」だけ |
| 実装コスト | プロパティごとにイベント+EventArgs が増える | 1 つのイベントで全プロパティをカバー |
UI バインディングが目的なら、まずは INotifyPropertyChanged を使うのが王道です。一方、「処理の意味に応じて、しっかりしたイベントを定義したい」ときには専用イベントを使う、と役割を分けて考えると整理しやすくなります。
実務でハマりやすいポイントとアンチパターン
プロパティ変更イベントは便利ですが、実務では次のような落とし穴がよく問題になります。ここでは代表的なものと対策をまとめます。
同じ値を何度も通知してしまう
ありがちなミスが、「同じ値が再セットされたときにもイベントが発火してしまう」問題です。通知を受ける側が UI 更新や重い処理を行っている場合、パフォーマンス低下や無駄な再描画につながります。
これを防ぐための基本テクニックが、setter での比較です。
set
{
if (_age == value)
{
return; // 値が変わらなければ何もしない
}
_age = value;
OnAgeChanged(_age);
}
特に UI バインディング系の ViewModel では、全プロパティにこの比較条件を入れることを強くおすすめします。共通化するために、ベースクラスに「値をセットして変更時だけ PropertyChanged を発火する」ヘルパーメソッドを用意することも多いです。
イベント購読しっぱなしによるメモリリーク
イベントを使うときに侮れないのが、購読解除漏れによるメモリリークです。長寿命オブジェクト(シングルトンや静的クラスなど)のイベントに短命オブジェクトが購読し、そのまま解除されないと、「参照が残り続けて GC されない」という状況が起きます。
回避策としては、次のようなものがあります。
- 寿命が長い側(イベントを発行する側)に、明示的な解除メソッドを用意し、不要になったタイミングで
-=を呼ぶ IDisposableを実装し、Dispose内で購読解除を行う- UI フレームワークが提供している「WeakEvent パターン」「イベントアグリゲーター」などを活用する
たとえば、購読側のクラスに IDisposable を実装し、破棄時に解除する例は次のようになります。
public class PersonObserver : IDisposable
{
private readonly Person _person;
public PersonObserver(Person person)
{
_person = person;
_person.AgeChanged += OnAgeChanged;
}
private void OnAgeChanged(object? sender, Person.AgeChangedEventArgs e)
{
Console.WriteLine($"Observer: {e.Age}");
}
public void Dispose()
{
_person.AgeChanged -= OnAgeChanged; // 購読解除を忘れない
}
}
イベントを多用するシステムでは、「イベントを購読したら、どこで解除するか」を設計段階で決めておくことが重要です。
マルチスレッドと UI スレッドの問題
バックグラウンドスレッドで処理を行い、途中経過をイベントで通知するようなケースでは、UI スレッドにマーシャリングする必要があるかどうか に注意してください。WPF や WinForms の UI コントロールは、基本的に UI スレッド以外から直接触ることができません。
そのため、「イベントはバックグラウンドスレッドから発火する」「イベントハンドラーの中で UI を触る」というコードを書くと、例外が発生したり、予期しない動作になったりします。この場合は、イベントを受け取った側で Dispatcher や SynchronizationContext を使って UI スレッドへ処理を移す設計にするのが一般的です。
シナリオ別:どのパターンを選ぶべきか
ここまで紹介した 3 つのパターンを、「どんなシナリオで使い分けるか」という観点で整理してみます。
| シナリオ | おすすめパターン | 理由 |
|---|---|---|
| ドメインモデルの状態変化をアプリ全体に通知したい | 専用イベント(EventHandler<TEventArgs>) | 意味のあるイベント名・引数で表現できるため、保守性が高い |
| WPF/MAUI などの UI バインディング | INotifyPropertyChanged | フレームワーク側が標準で対応しており、XAML バインディングと相性がよい |
| 小さなツールで、1 箇所にだけ通知を送りたい | 公開デリゲート(Action<T> など) | 実装が軽く、余計なイベントクラスを増やさなくてよい |
| テストコードや一時的な実験 | いずれのパターンでも可(簡単なほう) | 設計よりも素早い検証が目的なので、軽量なパターンで十分 |
特に業務システムでは、「とりあえず全部 INotifyPropertyChanged にする」ではなく、「このイベントはドメインとして意味があるか」「他のサービスに通知したいものか」といった観点も踏まえて設計しておくと、後から仕様変更が入ったときにコードが破綻しにくくなります。
汎用的なベースクラスを用意して楽をする
プロパティの変更検知ロジックは、どうしても同じようなコードが増えがちです。そのため、ベースクラスに共通のヘルパーメソッドを用意するのが実務ではよく行われます。特に INotifyPropertyChanged では定番です。
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
if (propertyName is null)
{
return;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// 値をセットし、変更があったときにだけ通知する共通メソッド
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
OnPropertyChanged(propertyName);
return true;
}
}
このベースクラスを継承すると、プロパティはかなり簡潔に書けます。
public class PersonViewModel2 : BindableBase
{
private int _age;
public int Age
{
get => _age;
set => SetProperty(ref _age, value);
}
private string? _name;
public string? Name
{
get => _name;
set => SetProperty(ref _name, value);
}
}
このように「プロパティ変更をトリガーにイベントを発火する」という仕組みも、ベースクラス化してしまえばコピペを減らせて、人為的なミス(比較漏れ・通知漏れ)もかなり抑えることができます。
まとめ:プロパティ変更をトリガーに処理を走らせるための指針
最後に、本記事のポイントを整理します。
- 「値の変更をトリガーに処理を走らせたい」場合、フィールドではなくプロパティの setter を入り口にするのが基本
- 値の変更検知は
if (_value == value) return;のように、本当に変わったときだけ通知する - 外部にも通知したいときは
EventHandler<TEventArgs>ベースのイベントを使うと安全かつ標準的 - UI バインディングが目的なら
INotifyPropertyChangedを実装するのが王道 - 小規模・内部限定なら、
Actionなどの公開デリゲートでシンプルに済ませるのもアリ - イベントの購読解除漏れはメモリリークの原因になるため、どこで解除するかを設計に組み込む
- よく使うパターンは、ベースクラスやヘルパーメソッドとして共通化しておくと実装もレビューも楽になる
これらのパターンを使い分ければ、「C# で変数(プロパティ)の変更をトリガーに処理を走らせたい」という要件は、UI・ドメイン・小規模ツールなど、さまざまな場面で過不足なく実現できます。自分が作っているアプリの性質に合わせて、適切な方式を選んでみてください。

コメント